skip to content
Site header image Nerdy Momo Cat

Making Webtrotion

By breaking things 10x a day

Last Updated:

Table of contents

Normally, if you want to add a page with Astro, you need to place an .astro- or .md- or .html- file in your src/pages-folder.
Main future goal Dec 11, 2023, 01:38 AM

Currently, the repo reads all blocks off notion, and writes them directly to renderable HTML. For saving re-computation of API calls, it saves API response to a json for posts. Instead of doing that, it might be ideal to write it as a content file that is rendered using MDX. Why? Because astro just released this Incremental Content Caching (Experimental). Caching the json, only caches the API request, but the build still often takes around 500ms at the very least. That would be the main incremental future goal from here on.

Nice to have #LATER

Support footnotes
Add option to remove caches from repo using github token
Attempt 1
name: Deploy to GitHub Pages

on:
  push:
    branches: [main]
  schedule:
    - cron: "0 */8 * * *"
  workflow_dispatch:

permissions:
  contents: read
  pages: write
  id-token: write

concurrency:
  group: "pages"
  cancel-in-progress: true

env:
  BUILD_PATH: "."
  NOTION_API_SECRET: ${{ secrets.NOTION_API_SECRET }}

jobs:
  build:
    runs-on: ubuntu-latest
    environment: github-pages
    timeout-minutes: 45
    steps:
      - name: Checkout your repository using git
        uses: actions/checkout@v4

      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: 21

      # Idea from https://www.voorhoede.nl/en/blog/super-fast-npm-install-on-github-actions/

      - name: Cache node manually
        id: npm-cache
        uses: actions/cache@v4
        with:
          path: ./node_modules
          key: modules-${{ hashFiles(format('{0}/package-lock.json', env.BUILD_PATH)) }}

      - name: Install dependencies
        if: steps.npm-cache.outputs.cache-hit != 'true'
        run: npm ci --ignore-scripts
        working-directory: ${{ env.BUILD_PATH }}

      - name: Setup Pages
        id: pages
        uses: actions/configure-pages@v5

      - name: Restore cached tmp
        if: steps.npm-cache.outputs.cache-hit == 'true'
        id: cache-tmp-restore
        uses: actions/cache/restore@v4
        with:
          path: ./tmp
          key: ${{ runner.os }}-${{steps.pages.outputs.origin}}-${{steps.pages.outputs.base_path}}-tmp-blogtrotion-${{ github.run_id }}-${{ github.run_attempt }}
          restore-keys: ${{ runner.os }}-${{steps.pages.outputs.origin}}-${{steps.pages.outputs.base_path}}-tmp-blogtrotion-

      - name: Restore cached public
        if: steps.npm-cache.outputs.cache-hit == 'true'
        id: cache-public-restore
        uses: actions/cache/restore@v4
        with:
          path: ./public
          key: ${{ runner.os }}-${{steps.pages.outputs.origin}}-${{steps.pages.outputs.base_path}}-public-blogtrotion-${{ github.run_id }}-${{ github.run_attempt }}
          restore-keys: ${{ runner.os }}-${{steps.pages.outputs.origin}}-${{steps.pages.outputs.base_path}}-public-blogtrotion-

      - name: Set and log environment secrets and variables and then Build with Astro and postbuild for pagefind
        env:
          NOTION_API_SECRET: ${{ secrets.NOTION_API_SECRET }}
          GITHUB_PAGES: true
          SITE: ${{ steps.pages.outputs.origin }}
          BASE: ${{ steps.pages.outputs.base_path }}
        run: |
          echo "NOTION_API_SECRET = $NOTION_API_SECRET"
          echo "GITHUB_PAGES = $GITHUB_PAGES"
          echo "SITE = $SITE"
          echo "BASE = $BASE"
          npx --no-install astro build
          # or if you prefer yarn, use: yarn build
          # --site and --base flags can be included if needed for Astro
          # --site steps.pages.outputs.origin" \
          # --base steps.pages.outputs.base_path"
          npx --no-install pagefind --site dist
        working-directory: ${{ env.BUILD_PATH }}

      - name: Save new tmp cache
        id: cache-tmp-save
        uses: actions/cache/save@v4
        with:
          path: ./tmp
          key: ${{ runner.os }}-${{steps.pages.outputs.origin}}-${{steps.pages.outputs.base_path}}-tmp-blogtrotion-${{ github.run_id }}-${{ github.run_attempt }}

      - name: Save new public cache
        id: cache-public-save
        uses: actions/cache/save@v4
        with:
          path: ./public
          key: ${{ runner.os }}-${{steps.pages.outputs.origin}}-${{steps.pages.outputs.base_path}}-public-blogtrotion-${{ github.run_id }}-${{ github.run_attempt }}

      - name: Upload artifact
        uses: actions/upload-pages-artifact@v3
        with:
          path: ${{ env.BUILD_PATH }}/dist

  deploy:
    needs: build
    runs-on: ubuntu-latest
    name: Deploy
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    steps:
      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v4

  workflow-keepalive:
    if: github.event_name == 'schedule'
    runs-on: ubuntu-latest
    permissions:
      actions: write
    steps:
      - uses: liskin/gh-workflow-keepalive@v1
Info
Synced blocks currently do not update due to cache
1 level Nested table of contents for headings (callouts, toggles, columns)
Links inside simple tables are not read right now
Cannot correctly figure out if there is interlinked content or not which leads to empty table of contents sometimes, fix that
Uploaded icons to pages do not show up in page mentions (linked ones do)
Very much later, possibly never: include random .md or .html files on page with the nice layout, tags, rss, sitemap, post listings etc.
Add gallery view instead of list view
Try adding this for images instead of optimizing myself?
And someday figure out lite embed for tiktok
Add lightbox expansion to youtube videos
Fix issue with notion.site being messed up because it thinks everything is part of its own site
There is no difference in API response, which is why 🤷‍♀️
Use tag descriptions for hover and tag page and collection description for collection page

Sadly, the descriptions are only returned with database, not with page properties 🤷‍♀️

Tag descriptions
Collection descriptions
Descriptions in OG images
Image links
Doesn’t show up in API
Fix this
Need to fix this: [WARN] Astro.request.headers is unavailable in "static" output mode, and in prerendered pages within "hybrid" and "server" output modes. If you need access to request headers, make sure that output is configured as either "server" or output: "hybrid" in your config file, and that the page accessing the headers is rendered on-demand. — started happening when I upgraded to astro 4.16 😠
Completed
In Dec 2024
Fix RSS, currently it only has the title and not the complete content and I DO NOT WANT THAT
Some internet stuff for this
In Nov 2024
Support for custom emojis
Liamm_top asked to see if there is a way to fix this for Chinese deployment which needs:
Fix the slug regex to support chinese characters Mar 8, 2024
Deploy to vercel
Collections are being cleared in slug for chinese characters and tags Mar 8, 2024
Icon has issues in satori when deploying on custom domains or has base URL? Mar 7, 2024
Remove pnpm Feb 25, 2024
OG image font for a different language Mar 7, 2024
In Sept 2024
Paste as mentions in notion doesn’t work?
The fudge, does not show up in API too?
Links pasted as mention don’t show up?
Links pasted as mention don’t show up?
In Aug 2024
#IMP
There is overscroll on chrome on iOS (not on safari, not even on arc). Aug 18, 2024 : apparently this is only on iPhone SE and I do not have the patience to figure it out
And the back to top button overlaps the toc button on pages,
and the toc size is too tall for iOS (because address bar thing and SE is tiny)
Currently the font size is fixed for any screen/viewport size but this shouldn’t be the case. so i need to find a way to fix that.
Search on mobile also has open tags which is irritating
Headings on same page should not show a popup, they should directly link to that position on click. The popup is useless in that case.

Problem? Currently there is no way to know if the linked block is on the same page or is on a different page; and the idea is we want the headings popup to show up if it is on a different page.

In June/July 2024
Issue with meta scraper sometimes (some URLs are fine in the same run)
Details

Seems like a kofi blocking thing? Yeah and found Jul 25, 2024 that even using dub.sh or something like that also does not work

I tried doing something like this but I do not like how it looks:

<!--Commenting here so that it does not inject -->
<!DOCTYPE html> <!-- inject -->
<script src='https://storage.ko-fi.com/cdn/scripts/overlay-widget.js'></script>
<script>
  kofiWidgetOverlay.draw('nerdymomocat', {
    'type': 'floating-chat',
    'floating-chat.donateButton.text': 'Buy me a coffee',
    'floating-chat.donateButton.background-color': '#F3E1C6',
    'floating-chat.donateButton.text-color': '#4F1116'
  });
</script>
i tried this too but did not like how long af it was\
<!--Commenting here so that it does not inject -->
<!DOCTYPE html> <!-- inject -->
<a
  href="https://ko-fi.com/nerdymomocat"
  target="_blank"
  rel="noopener noreferrer"
  class="flex w-full min-w-0 max-w-full grow select-none items-stretch overflow-hidden rounded border border-gray-200 no-underline dark:border-gray-800"
>
  <div
    class="flex-bookmark-text overflow-hidden p-3 text-left text-textColor/90"
  >
    <div
      class="mb-0.5 h-6 overflow-hidden truncate whitespace-nowrap leading-5"
    >
      Support Nerdy Momo Cat on Ko-fi! Buy her a hot chocolate ❤️
    </div>
    <div
      class="h-8 overflow-hidden text-xs leading-4 opacity-80"
    >
      Support Nerdy Momo Cat On Ko-fi. Ko-fi lets you support the people and causes you love with small donations
    </div>
    <div class="mt-1.5 flex max-w-full">
      <div class="mr-1.5 h-4 w-4 min-w-4">
        <img
          class="inline-block max-w-full"
          loading="lazy"
          src="https://www.google.com/s2/favicons?domain=ko-fi.com"
          alt="title"
          decoding="async"
        />
      </div>
      <div
        class="overflow-hidden truncate whitespace-nowrap text-xs leading-4"
      >
        https://ko-fi.com/
      </div>
    </div>
  </div>
  <div
    class="flex-bookmark-image relative hidden sm:block"
  >
    <img
      class="!absolute h-full w-full object-contain object-right"
      loading="lazy"
      src="https://storage.ko-fi.com/cdn/useruploads/138404e3-801f-4922-beb3-0db70efc0578_ed507fa6-95c6-4a2f-9dc9-e97d8d19cab4.png"
      alt="title"
      decoding="async"
    />
  </div>
</a>

thought about using this but i do not like how it adds extra useless javascript

<!--Commenting here so that it does not inject -->
<!DOCTYPE html> <!-- inject -->
<script type='text/javascript' src='https://storage.ko-fi.com/cdn/widget/Widget_2.js'></script><script type='text/javascript'>kofiwidget2.init('Support my bread addiction', '#4A1510', 'I2I6X8MM3');kofiwidget2.draw();</script> 

this looks ugly

<!--Commenting here so that it does not inject -->
<!DOCTYPE html> <!-- inject -->
<div>
    <a href="https://www.buymeacoffee.com/yourusername" target="_blank" rel="noopener noreferrer"
        style="
            background-color: #f59e0b;
            color: white;
            font-weight: 500;
            border-radius: 6px;
            padding: 8px 16px;
            display: inline-flex;
            align-items: center;
            justify-content: center;
            text-decoration: none;
            transition: background-color 0.3s ease-in-out;
            box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
        "
        onmouseover="this.style.backgroundColor='#d97706'"
        onmouseout="this.style.backgroundColor='#f59e0b'"
    >
        <span style="font-size: 1.125rem; margin-right: 8px;">☕</span>
        Buy me a coffee
    </a>
</div>
Fix plain text does not match, using plaintext
scrolling expanding table of contents (this can be done using intersection observer — I am not sure it is worth doing though? Or it might be)
Fix the distance between toc and toc button on mobile
Fix:

[Shiki] 10 instances have been created. Shiki is supposed to be used as a singleton, consider refactoring your code to cache your highlighter instance; Or call highlighter.dispose() to release unused instances.

Solved — use this transformers= {TRANSFORMER_FUNCTIONS_ARR} instead of changing array based on shortcode because it creates a new highlighter then.

⚠️ keep an eye on build times though!
And maybe move metascraper out and just use noembed instead?
Starlight also has an interesting mobile menu implementation btw
Add sort, filter to tables?
In March 2024
Clicking read more (on mobile at least) does not close the popup?
dynamically load og image fonts
Fix OG image generation with the logo
add a print stylesheet
Set the bookmarks to take full width?
Add error fixing to astro notion tweet, because if someone deletes the tweet that has been embedded, the build stops
Fix web-bookmark sizing being all over the place
Tags on the page when they wrap do not have vertical spacing
Do not height restrict mermaid previews
bold headings by default
extend bookmark full width if no image

Menu bar on mobile only clicks on words not the whole line
sticky header (CANCELLED - I DO NOT LIKE THIS MUCH)
referenced pages, referenced in pages etc
Remember for later, we did not move collections out like tags because we would kinda need to have all collections list page and that makes no sense
Oh, it already gives an error anyway
html embedded into iframe rather than inserted into div - some 5 min exploration did not work
Fix lightbox
google maps embed
pdf embed
OG only for recent entries in single page preview collections

Lol! Later? I did it Nov 28, 2023, 05:44 AM just because it was absolutely NOT needed

Only run cron if anything in notion has been updated (might need to look into build adapters)
incremental builds (On-Demand Incremental Static Regeneration) when astro implements them

kinda doing them right now with aggressive caching of API requests at elast

Jun 14, 2024
Pinned posts for a collection?
Email clicks should copy email and show a toast, not open a mailto: link.
When linking to a block that is a toggle, and it is shown in popup, it feels weird, that I can click the arrow, but nothing opens up

Astro

These are some links that are important to consider for astro:

Some other websites with same theme

Checking out other stuff

I could have used Nuxt or Next or maybe even hugo but I don’t know why I do not want to use them Oct 27, 2023, 06:58 AM
Let me start with basic questions

Do I need to use npm or can I do styling with CSS/Tailwind and generate mds using n2y?

And what happens if I do that?

What is not supported in mdx basically?

I will need to fix this stuff

class CalloutBlock(Block):
    def __init__(self, client, notion_data, page, get_children=True):
        super().__init__(client, notion_data, page, get_children)
        self.rich_text = client.wrap_notion_rich_text_array(
            self.notion_type_data["rich_text"], self
        )
        # the color and icon are not currently used

    def to_pandoc(self):
        content = self.rich_text.to_pandoc()
        if self.has_children:
            children = self.children_to_pandoc()
            result = [Para(content)]
            result.extend(children)
        else:
            result = Para(content)
        return result

class AudioBlock(ContentBlock):
    pass


class VideoBlock(ContentBlock):
    pass
class ToggleBlock(Block):
    """
    Generates a bulleted list item with indented children. A plugin may be used
    to add html classes and replicate the interactive behavior found in Notion.
    """

    def __init__(self, client, notion_data, page, get_children=True):
        super().__init__(client, notion_data, page, get_children)
        self.rich_text = client.wrap_notion_rich_text_array(
            self.notion_type_data["rich_text"], self
        )

    def to_pandoc(self):
        header = self.rich_text.to_pandoc()
        children = self.children_to_pandoc()
        content = [Para(header)]
        content.extend(children)
        return BulletList([content])

Components and libraries for astro

Comments with giscus

Other components

Log

Nov 22, 2023, 01:12 PM

Files that still have original style

bookmark.astro
callout.astro
Nov 22, 2023, 08:48 PM
Nov 23, 2023, 09:02 PM
Initial stuff about filter done but remember it sometimes gives error, so figure that out!
Default slugs
Nov 24, 2023, 02:32 AM
same page href
code copy
i added svg to make it look nice AND GRR
The issue you're encountering is due to event delegation. When you click on the svg or path, they become the target of the event, not the button. Since the data-code attribute is on the button, clicking on the svg results in null because ev.target is the svg which does not have the data-code attribute. To consistently get the data-code attribute from the button regardless of where the click occurs (on the button or its child elements), you should use ev.currentTarget instead of ev.target. The currentTarget is always the element to which the event handler has been attached, in this case, the button.
fix code highlight! move to shiki because that is default with astro!

including import "../styles/syntax-coloring.css"; worked! but i am still gonna try a tiny bit to move to shiki!

nah prism is good enough!

WTF is the z-index changing to be on top of the header on mobile man?

Well, I found a nice svg and thought, hey, i can start with -90 degree angle, then rotate it on click, BIG MISTAKE!

<svg class="-rotate-90  transform transition-all duration-300 group-open:rotate-180" fill="none" height="20" width="20" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewBox="0 0 24 24">
 <polyline points="6 9 12 15 18 9"></polyline>
</svg>
Nov 24, 2023, 10:22 AM
tags color in notion to tags color on page
on post
on blog tags
Nov 24, 2023, 07:16 PM
If I want notes on my homepage which I very well might, change here src/pages/[...page].astro.
how do i embed any html in page?
Image optimization maybe Nov 24, 2023 (I used astro image tools, I am not sure it works though?)
Stream/JSON/preview — i really do not know the solution
YAY! I DID IT!

So, I think initially I was thinking of using notion relations and items and sub-items to load them into a single page. Instead I have an idea. What if the collection page preview shows the complete content/full post instead of just the preview? That basically solves our problem, right?

Remove notion extras
Figure out how to create post-preview with all blocks

I think this is where the information is: src/pages/posts/collection/ [collection]/ [...page].astro

This seems to be the post preview code
<ul class="space-y-8 text-start">
  { page.data.map((post) => (
  <li class="flex flex-col flex-wrap gap-2 sm:flex-row [&_q]:basis-full">
    <PostPreview post="{post}" as="h2" withDesc />
  </li>
  )) }
</ul>
This is post preview. astro
---
// import type { CollectionEntry } from "astro:content";
// import type { HTMLTag, Polymorphic } from "astro/types";
import type { HTMLTag } from "astro/types";
import FormattedDate from "../FormattedDate.astro";
import type { Post } from "src/lib/interfaces";
import { getPostBySlug } from "@/lib/notion/client";

// type Props<Tag extends HTMLTag> = Polymorphic<{ as: Tag }> & {
// 	post: CollectionEntry<"post">;
// 	withDesc?: boolean;
// };
//FIXME: why is this a new method?

interface Props {
	post: Post;
	as?: HTMLTag;
	withDesc?: boolean;
}

const { post, as: Tag = "div", withDesc = false } = Astro.props;

//TODO: what is this?
// const postDate = post.data.updatedDate ?? post.data.publishDate;
---
<FormattedDate date={new Date(post.Date)} class="min-w-[120px] text-gray-500" />
<Tag>
	<a
		href={`/posts/${post.Slug}/`}
		class="cactus-link"
		rel="prefetch"
	>
		{post.Title}
	</a>
</Tag>
{withDesc && <q class="line-clamp-3 block italic">{post.Excerpt}</q>}
This is how it is done for direct root pages
<PageLayout meta={{ title: page.Title || siteConfig.title }}>
	<section>
		<NotionBlocks blocks={blocks} isRoot={true} />
	</section>
	<!-- {
    //NOTE: This added a recent posts thing to my page. I can add it back later if needed
		!slug && (
			<section aria-label="Blog post list" class="mt-16">
				<h2 class="title mb-4 text-xl">Posts</h2>
				<ul class="space-y-4 sm:space-y-2">
					{rankedPosts.map((p) => (
						<li class="flex flex-col gap-x-2 sm:flex-row">
							<PostPreview post={p} />
						</li>
					))}
				</ul>
			</section>
		)
	} -->
</PageLayout>
Figure out how to create post preview with all blocks for only particular collections
toggle issue, if i toggle parent, the arrow for all child blocks toggles too (the content does not show, but it does toggle)
HTML Injection?
Figure out later how to scope these scripts or load these into iframe instead maybe?
Should these scripts be at a component level or the whole blogpost level?

Me:

<script>
	import mermaid from "mermaid";
	mermaid.initialize({ startOnLoad: true, theme: "neutral" });
</script>

<script>
  document.querySelectorAll("button.copy").forEach((button) => {
  button.addEventListener("click", (ev) => {
    const svg = button.querySelector('.svg-icon path');
    const code = ev.currentTarget.getAttribute("data-code");
    navigator.clipboard.writeText(code);
    if (svg) {
      svg.setAttribute("fill", "limegreen");
      setTimeout(() => {
        svg.setAttribute("fill", "white");
      }, 1000);
    }
  });
});
</script>

ChatGPT:

Deciding whether to place scripts at the component level or at the whole blog post level in an Astro project depends on the specific use case and context of the scripts. Here are some guidelines to help you make this decision for the two scripts you've mentioned:

  1. Mermaid Initialization Script:
    • If the Mermaid charts are used only within a specific component and not throughout the entire blog post, it makes sense to keep the Mermaid initialization script at the component level. This way, the script will only run where it's needed, which can improve performance and maintainability.
    • If Mermaid charts are a common feature across various parts of your blog post or site, you might consider placing this script at a higher level (like the layout or page level) so that it's available throughout the blog post or across multiple pages.
  2. Button Copy Script:
    • If the copy functionality for code blocks is specific to a component (like a code snippet component), then it's appropriate to have this script at the component level. This approach encapsulates the functionality within the component, making it reusable and easy to manage.
    • However, if you have a similar copy functionality required in multiple components across the blog post, you might want to centralize this script at the blog post level to avoid redundancy and keep the code DRY (Don't Repeat Yourself).

Here are some additional considerations:

  • Performance and Load Times: Keeping scripts at the component level ensures they're only loaded and run when the component is used, which can be beneficial for performance.
  • Reusability: If you foresee the need to use these features (like Mermaid charts or copy buttons) in multiple components across different parts of your site, extracting the scripts to a higher level and making them globally available might be more efficient.
  • Maintainability: Keeping scripts close to where they are used enhances maintainability, as it's easier to understand and modify the code when everything related to a particular feature is in one place.

Ultimately, the decision depends on the specific requirements of your project and how you plan to scale or maintain it in the future.

If you have multiple <AlertButton /> components on a page, Astro will not run the script multiple times. Scripts are bundled and only included once per page. Using querySelectorAll ensures that this script attaches the event listener to every button with the alert class found on the page. GRRRR (link here)
Make sure all onclick are either inside the element or have selector all.
Move tweets to static tweet-astro
Nov 25, 2023, 12:13 AM
fix site properties !MAJOR

In the original otoyo’s repo, those things are in src/layouts/Layout.astro

Can’t use astro global in typescript 😠

getSite is repeated in blog-helpers.ts and astro.config.ts at the moment.

Somehow could not run getDatabase in site.config.ts; so I created a file of my own: siteInfoSetup

THE WHOLE FUDGING ISSUE WAS IN DATE.TS NOT IN CONFIG FILE

Now it is not running at all?

So, there is a circular dependency, getNavLink was calling pathJoin in index, which was trying to import siteInfo.ts. Awesome

temp code holder
import { getDatabase } from "@/lib/notion/client";
import { getNavLink, getSite } from "@/lib/blog-helpers";
import { SiteInfo } from "@/types";

export async function siteInfoFunc(): Promise<SiteInfo> {
  const tl = "", ds = "", path = "/", oim = "";
  const database = await getDatabase();

  const rawTitle = database.Title.includes("||") ? database.Title.split(" || ")[0] : database.Title;
  const siteAuthor = database.Title.includes("||") ? database.Title.split(" || ")[1] : "";
  const siteTitle = tl ? `${tl} - ${rawTitle}` : rawTitle;
  const siteDescription = ds ? ds : database.Description;
  const siteURL = new URL(getNavLink(path), getSite()).toString();

  // console.log(siteTitle, siteDescription, siteURL);


  // export const siteInfo = () => {
  //   return [siteTitle, siteDescription, siteURL];
  // }

  const siteInfoF: SiteInfo = {
    title: siteTitle,
    description: siteDescription,
    author: siteAuthor,
    lang: "en",
    homePageSlug: "index",
    // Include view-transitions: https://docs.astro.build/en/guides/view-transitions/
    includeViewTransitions: false,
    // Meta property, found in src/components/BaseHead.astro L:42
    ogLocale: "en",
    // Date.prototype.toLocaleDateString() parameters, found in src/utils/date.ts.
    date: {
      locale: "en",
      options: {
        day: "numeric",
        month: "short",
        year: "numeric",
      },
    },
    webmentions: {
      link: "https://webmention.io/astro-cactus.chriswilliams.dev/webmention",
      //  // site: "https://astro-cactus.chriswilliams.dev/",
    },
  };
  return siteInfoF;
}
dump site.config.ts original
import type { SiteConfig } from "@/types";


export const siteConfig: SiteConfig = {
  // Used as both a meta property (src/components/BaseHead.astro L:31 + L:49) & the generated satori png (src/pages/og-image/[slug].png.ts)
  //todo: get this from notion maybe? and we do not need locale.
  author: "Chris Williams",
  // Meta property used to construct the meta title property, found in src/components/BaseHead.astro L:11
  title: "Astro Theme Cactus",
  // Meta property used as the default description meta property
  description: "An opinionated starter theme for Astro",
  // HTML lang property, found in src/layouts/Base.astro L:18
  lang: "en-GB",
  // Meta property, found in src/components/BaseHead.astro L:42
  ogLocale: "en_GB",
  // Date.prototype.toLocaleDateString() parameters, found in src/utils/date.ts.
  date: {
    locale: "en-GB",
    options: {
      day: "numeric",
      month: "short",
      year: "numeric",
    },
  },
  homePageSlug: "index",
  // Include view-transitions: https://docs.astro.build/en/guides/view-transitions/
  includeViewTransitions: false,
  webmentions: {
    link: "https://webmention.io/astro-cactus.chriswilliams.dev/webmention",
  },
};

So, now after 6 hours, we are at a point, where build works but dev does not and I do not know why.

Cannot read properties of undefined (reading 'date') It has something to do with date. I am not sure what.

Error dump
npm run dev
astro-notion-cms-cactus@3.5.0 dev
astro dev
🚀  astro  v3.4.0 started in 491ms

 Local    http://localhost:4321/
 Network  use --host to expose

(node:44831) [DEP0040] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead.
(Use `node --trace-deprecation ...` to show where the warning was created)
8:57:33 PM [vite] Error when evaluating SSR module /src/utils/date.ts:
|- TypeError: Cannot read properties of undefined (reading 'date')
at eval (/Users/nerdymomocat/Documents/Websites/astro-notion-cms-copy-to-cactus-theme/src/utils/date.ts:5:75)
at async instantiateModule (file:///Users/nerdymomocat/Documents/Websites/astro-notion-cms-copy-to-cactus-theme/node_modules/vite/dist/node/chunks/dep-bb8a8339.js:56052:9)

8:57:33 PM [vite] Error when evaluating SSR module /src/utils/index.ts: failed to import "/src/utils/date.ts"
|- TypeError: Cannot read properties of undefined (reading 'date')
at eval (/Users/nerdymomocat/Documents/Websites/astro-notion-cms-copy-to-cactus-theme/src/utils/date.ts:5:75)
at async instantiateModule (file:///Users/nerdymomocat/Documents/Websites/astro-notion-cms-copy-to-cactus-theme/node_modules/vite/dist/node/chunks/dep-bb8a8339.js:56052:9)

8:57:33 PM [vite] Error when evaluating SSR module /src/lib/notion/client.ts: failed to import "/src/utils/index.ts"
|- TypeError: Cannot read properties of undefined (reading 'date')
at eval (/Users/nerdymomocat/Documents/Websites/astro-notion-cms-copy-to-cactus-theme/src/utils/date.ts:5:75)
at async instantiateModule (file:///Users/nerdymomocat/Documents/Websites/astro-notion-cms-copy-to-cactus-theme/node_modules/vite/dist/node/chunks/dep-bb8a8339.js:56052:9)

8:57:33 PM [vite] Error when evaluating SSR module /src/site.config.ts: failed to import "/src/lib/notion/client.ts"
|- TypeError: Cannot read properties of undefined (reading 'date')
at eval (/Users/nerdymomocat/Documents/Websites/astro-notion-cms-copy-to-cactus-theme/src/utils/date.ts:5:75)
at async instantiateModule (file:///Users/nerdymomocat/Documents/Websites/astro-notion-cms-copy-to-cactus-theme/node_modules/vite/dist/node/chunks/dep-bb8a8339.js:56052:9)

8:57:33 PM [vite] Error when evaluating SSR module /src/components/BaseHead.astro: failed to import "/src/site.config.ts"
|- TypeError: Cannot read properties of undefined (reading 'date')
at eval (/Users/nerdymomocat/Documents/Websites/astro-notion-cms-copy-to-cactus-theme/src/utils/date.ts:5:75)
at async instantiateModule (file:///Users/nerdymomocat/Documents/Websites/astro-notion-cms-copy-to-cactus-theme/node_modules/vite/dist/node/chunks/dep-bb8a8339.js:56052:9)

8:57:33 PM [vite] Error when evaluating SSR module /src/layouts/Base.astro: failed to import "/src/components/BaseHead.astro"
|- TypeError: Cannot read properties of undefined (reading 'date')
at eval (/Users/nerdymomocat/Documents/Websites/astro-notion-cms-copy-to-cactus-theme/src/utils/date.ts:5:75)
at async instantiateModule (file:///Users/nerdymomocat/Documents/Websites/astro-notion-cms-copy-to-cactus-theme/node_modules/vite/dist/node/chunks/dep-bb8a8339.js:56052:9)

8:57:33 PM [vite] Error when evaluating SSR module /Users/nerdymomocat/Documents/Websites/astro-notion-cms-copy-to-cactus-theme/src/pages/[...page].astro: failed to import "/src/layouts/Base.astro"
|- TypeError: Cannot read properties of undefined (reading 'date')
at eval (/Users/nerdymomocat/Documents/Websites/astro-notion-cms-copy-to-cactus-theme/src/utils/date.ts:5:75)
at async instantiateModule (file:///Users/nerdymomocat/Documents/Websites/astro-notion-cms-copy-to-cactus-theme/node_modules/vite/dist/node/chunks/dep-bb8a8339.js:56052:9)

error   Cannot read properties of undefined (reading 'date')
File:
/Users/nerdymomocat/Documents/Websites/astro-notion-cms-copy-to-cactus-theme/src/utils/date.ts:9:53
Code:
> 9 | const dateFormat = new Intl.DateTimeFormat(siteInfo.date.locale, siteInfo.date.options);
|                                                     ^
11 | export function getFormattedDate(
12 |   date: string | number | Date,
Stacktrace:
TypeError: Cannot read properties of undefined (reading 'date')
at /Users/nerdymomocat/Documents/Websites/astro-notion-cms-copy-to-cactus-theme/src/utils/date.ts:9:53
at async instantiateModule (file:///Users/nerdymomocat/Documents/Websites/astro-notion-cms-copy-to-cactus-theme/node_modules/vite/dist/node/chunks/dep-bb8a8339.js:56052:9)


https://docs.astro.build/en/guides/data-fetching/#:~:text=💡 Take advantage of top-level await inside of your Astro component script.

https://docs.astro.build/en/guides/configuring-astro/#environment-variables
https://docs.astro.build/en/guides/configuring-astro/#environment-variables

So basically, astro config can only be static in dev mode at least, in build mode I can use github process variables. But I need database information? So, I do not really know what to do about it?

Like the getSite we wrote technically doesn’t really work?

export const CUSTOM_DOMAIN =
  import.meta.env.CUSTOM_DOMAIN || process.env.CUSTOM_DOMAIN || 'alpacat.com' // <- Set your costom domain if you have. e.g. alpacat.com
export const BASE_PATH =
  import.meta.env.BASE_PATH || process.env.BASE_PATH || '' // <- Set sub directory path if you want. e.g. /docs/
import { CUSTOM_DOMAIN, BASE_PATH } from "./src/constants";

const getSite = function () {
  if (CUSTOM_DOMAIN) {
    return new URL(BASE_PATH, `https://${CUSTOM_DOMAIN}`).toString();
  }

  if (process.env.VERCEL && process.env.VERCEL_URL) {
    return new URL(BASE_PATH, `https://${process.env.VERCEL_URL}`).toString();
  }

  if (process.env.CF_PAGES) {
    if (process.env.CF_PAGES_BRANCH !== 'main') {
      return new URL(BASE_PATH, process.env.CF_PAGES_URL).toString();
    }

    return new URL(
      BASE_PATH,
      `https://${new URL(process.env.CF_PAGES_URL).host
        .split('.')
        .slice(1)
        .join('.')}`
    ).toString();
  }

  return new URL(BASE_PATH, 'http://localhost:4321').toString();
};

So, right now it works with me using export key=value on CLI

Nov 25, 2023, 08:26 AM
Do not show pages with slug starting with underscore in
collection index page,
posts page,
tag page,
rss,
sitemap
fix robots.txt somehow?
check audio embeds
So, I've got this mobile button that opens and closes a navigation bar when I click on it. But here's the thing - when the menu is open, I want it to close if I click anywhere outside of it.
giscus
how do i use webmentions?
JSON options rather than everything in env? Only Notion values in env!
Look at the cache stuff in otoyo’s post. Maybe it is just for cloudfare? Idk!
Change getMenu to be: instead of excluding pages with no rank or rank 0, just set ranks of those to 99. remember that homePageSlug if available will always have the top rank.
Fix post preview for empty desc
THEME AND FONTS!
Notion API does not say that block type can be audio , add audio support man
This is duplicated across 5 files, maybe fix it later?
const fileAtacchedBlocks = extractTargetBlocks("image", blocks)
	.concat(extractTargetBlocks("video", blocks))
	.concat(extractTargetBlocks("file", blocks))
	.filter((block) => {
		if (!block) {
			return false;
		}
		const imageOrVideoOrAudioOrFile = block.NImage || block.File || block.Video || block.Audio;
		return (
			imageOrVideoOrAudioOrFile &&
			imageOrVideoOrAudioOrFile.File &&
			imageOrVideoOrAudioOrFile.File.Url
		);
	});

// Download files
await Promise.all(
	fileAtacchedBlocks
		.map(async (block) => {
			const expiryTime = (block.NImage || block.File || block.Video || block.Audio).File.ExpiryTime;
			if (Date.parse(expiryTime) > Date.now()) {
				return Promise.resolve(block);
			}
			return getBlock(block.Id);
		})
		.map((promise) =>
			promise.then((block) => {
				let url!: URL;
				try {
					url = new URL((block.NImage || block.File || block.Video || block.Audio).File.Url);
				} catch (err) {
					console.log("Invalid file URL");
					return Promise.reject();
				}
				return Promise.resolve(url);
			}),
		)
		.map((promise) => promise.then(downloadFile)),
);
Side note, apparently if it is a video embed but not youtube, it is not rendered? Maybe call the generic embed in that case?
Code here
videoUrl &&
			(url ? (
				isYouTubeURL(url) ? (
					<iframe
          class="w-full h-[340px] max-w-sm:h-[220px]"
						src={`https://www.youtube.com/embed/${parseYouTubeVideoId(url)}`}
						title="YouTube video player"
						allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
						allowfullscreen
					/>
				) : null
Nov 25, 2023, 07:39 PM
Issue with toggle 2 headings? (FIXED! I had not changed the polyline for some reason!)
Do not show pages with slug starting with underscore in
collection index page,
posts page,
tag page,
rss,
sitemap
fix robots.txt somehow?
giscus
how do i use webmentions?
links to main pages and pages as URL seems to be broken?
OG image with satori
og image creation using satori

I don’t get how astro satori works 🤷‍♀️

Stuff like this is more complicated than the existing one. So, I am gonna try to figure out the existing one!

Need to change this function to have all of them:

export const getStaticPaths: GetStaticPaths = async () => {
  const posts = await getAllPosts();
  return posts.filter((post) => !post.FeaturedImage).map(({ Slug }) => ({ params: { slug: Slug } }));
};
Made huge progress on how to include image on side
<div
    style={{
      height: "100%",
      width: "100%",
      display: "flex",
      backgroundColor: "white",

      backgroundSize: "100px 100px",
      fontFamily: "JetBrainsMono-Bold",
    }}
  >
    <div
      style={{
        padding: "20px",
        display: "flex",
        width: "100%",
        height: "100%",
        justifyContent: "center",
        alignItem: "stretch",
      }}
    >

      <div
        style={{
          display: "flex",
          flexDirection: "row",
          justifyContent: "space-between",
          border: "1px solid #374151",
          boxShadow: "5px 5px 0px #374151",
          width: "100%",
          height: "100%",
          padding: "10px",
        }}
      >
      <div style={{display:"flex", flex: 1}}>
              <img src="https://nerdymomocat.github.io/images/me.png" width={400} height={400} style={{
      width: '100%', 
      height: '100%', 
      objectFit: 'contain', 
      objectPosition: 'center'
    }} />

      </div>
      <div style={{display:"flex", flexDirection:"column", flex: 1,}}>
        <div
          style={{
            fontSize: "32px",
            fontWeight: "900",
            lineHeight: "3rem",
            padding: "10px 0 50px 0",
            color: "#374151",
            flex: 1,
            display: "flex",
            fontFamily: "PlusJakartaSans",
          }}
        >
          Demo New Page: Trying to get Astro+Notion working how I want to
        </div>
        <div
          style={{
            fontSize: "16px",
            fontWeight: "900",
            color: "#374151",
            display: "flex",
            flexDirection: "row",
            justifyContent: "space-between",
            alignItems: "center",
          }}
        >
          <div>
            DATE HERE
          </div>
          <div style={{ display: "flex", alignItems: "center" }}>
            <span style={{ marginRight: "16px" }}>Jafar Aziz</span>
          </div>
        </div>
        </div>
      </div>
    </div>
  </div>
Next up, image as background blur or maybe a div on bottom?
contain does not work, i need to set specific size for it to fit by ratio? →no assume it is a square, if not a square, it goes into background mode!
This works with HTML native, but not with svg or svg→png
<div
  style={{
    position: 'relative',
    height: "100%",
    width: "100%",
    display: "flex",
    fontFamily: "JetBrainsMono-Bold",
  }}
>
  <div
    style={{
      position: 'absolute',
      top: 0,
      left: 0,
      height: '100%',
      width: '100%',
      backgroundImage: `url('https://nerdymomocat.github.io/images/me.png')`,
      backgroundRepeat: 'no-repeat',
      backgroundSize: 'contain',
      backgroundPosition: 'center',
      filter: 'blur(8px)',
      zIndex: -1, // Ensures this div is behind the content
      backgroundColor: "white"
    }}
  ></div>
    <div
      style={{
        padding: "20px",
        display: "flex",
        width: "100%",
        height: "100%",
        justifyContent: "center",
        alignItem: "stretch",
      }}
    >

      <div
        style={{
          display: "flex",
          flexDirection: "row",
          justifyContent: "space-between",
          border: "1px solid #374151",
          boxShadow: "5px 5px 0px #374151",
          width: "100%",
          height: "100%",
          padding: "10px",
        }}
      >

      <div style={{display:"flex", flexDirection:"column", flex: 1,}}>
        <div
          style={{
            fontSize: "32px",
            fontWeight: "900",
            lineHeight: "3rem",
            padding: "10px 0 50px 0",
            color: "#374151",
            flex: 1,
            display: "flex",
            fontFamily: "PlusJakartaSans",
          }}
        >
          Demo New Page: Trying to get Astro+Notion working how I want to
        </div>
        <div
          style={{
            fontSize: "16px",
            fontWeight: "900",
            color: "#374151",
            display: "flex",
            flexDirection: "row",
            justifyContent: "space-between",
            alignItems: "center",
          }}
        >
          <div>
            DATE HERE
          </div>
          <div style={{ display: "flex", alignItems: "center" }}>
            <span style={{ marginRight: "16px" }}>Jafar Aziz</span>
          </div>
        </div>
        </div>
      </div>
    </div>
  </div>

Also, There is no z-index support in SVG. Elements that come later in the document will be painted on top.

await satori(
  <img src="data:image/png;base64,..." width={200} height={300} />,
  // Or src={arrayBuffer}, src={buffer}
  options
)
Masking with white image or white linear translucent layer somehow does not work?
Somehow this started working, we do not question it, we roll with it!
<div
  style={{
    height: '100%',
    width: '100%',
    display: 'flex',
    flexDirection: 'column',
    alignItems: 'center',
    justifyContent: 'center',
    fontSize: 32,
    fontWeight: 600,
    backgroundColor: 'white'
  }}
>
<div
    style={{
      position: 'absolute', // Absolutely positioned
      top: 0,
      left: 0,
      height: '100%',
      width: '100%',
      background: 'url(https://maggieappleton.com/images/posts/squish-structure/sms2.62.jpeg)',
      backgroundRepeat: "no-repeat",
      maskImage: 'linear-gradient(to bottom, rgba(255, 255, 255, 0.3), rgba(255, 255, 255, 0.3))',
      backgroundColor: 'white'
    }}
  ></div>

<div
      style={{
        padding: "20px",
        display: "flex",
        width: "100%",
        height: "100%",
        justifyContent: "center",
        alignItem: "stretch",
      }}
    >

      <div
        style={{
          display: "flex",
          flexDirection: "row",
          justifyContent: "space-between",
          border: "1px solid #374151",
          boxShadow: "5px 5px 0px #374151",
          width: "100%",
          height: "100%",
          padding: "10px",
        }}
      >

      <div style={{display:"flex", flexDirection:"column", flex: 1,}}>
        <div
          style={{
            fontSize: "px",
            fontWeight: "900",
            lineHeight: "3rem",
            padding: "10px 0 50px 0",
            color: "#374151",
            flex: 1,
            display: "flex",
            fontFamily: "PlusJakartaSans",
          }}
        >
          Demo New Page: Trying to get Astro+Notion working how I want to
        </div>
        <div
          style={{
            fontSize: "16px",
            fontWeight: "900",
            color: "#374151",
            display: "flex",
            flexDirection: "row",
            justifyContent: "space-between",
            alignItems: "center",
          }}
        >
          <div>
            DATE HERE
          </div>
          <div style={{ display: "flex", alignItems: "center" }}>
            <span style={{ marginRight: "16px" }}>Nerdy Momo Cat</span>
          </div>
        </div>
        </div>
      </div>
    </div>
</div>
And this too! (but it looks weird with colored background images)
<div
  style={{
    height: '100%',
    width: '100%',
    display: 'flex',
    flexDirection: 'column',
    alignItems: 'center',
    justifyContent: 'center',
    fontSize: 32,
    fontWeight: 600,
    backgroundColor: 'white'
  }}
>
   <img src="https://maggieappleton.com/images/posts/squish-structure/sms2.66.jpeg" style={{position: 'absolute', // Absolutely positioned
      top: 0,
      left: 0,
      height: '100%',
      width: '100%',
      objectFit: 'contain', 
      objectPosition: 'center',
      maskImage: 'linear-gradient(to bottom, rgba(255, 255, 255, 0.2), rgba(255, 255, 255, 0.2))'
    }} />

<div
      style={{
        padding: "20px",
        display: "flex",
        width: "100%",
        height: "100%",
        justifyContent: "center",
        alignItem: "stretch",
      }}
    >

      <div
        style={{
          display: "flex",
          flexDirection: "row",
          justifyContent: "space-between",
          border: "1px solid #374151",
          boxShadow: "5px 5px 0px #374151",
          width: "100%",
          height: "100%",
          padding: "10px",
        }}
      >

      <div style={{display:"flex", flexDirection:"column", flex: 1,}}>
        <div
          style={{
            fontSize: "px",
            fontWeight: "900",
            lineHeight: "3rem",
            padding: "10px 0 50px 0",
            color: "#374151",
            flex: 1,
            display: "flex",
            fontFamily: "PlusJakartaSans",
          }}
        >
          Demo New Page: Trying to get Astro+Notion working how I want to
        </div>
        <div
          style={{
            fontSize: "16px",
            fontWeight: "900",
            color: "#374151",
            display: "flex",
            flexDirection: "row",
            justifyContent: "space-between",
            alignItems: "center",
          }}
        >
          <div>
            DATE HERE
          </div>
          <div style={{ display: "flex", alignItems: "center" }}>
            <span style={{ marginRight: "16px" }}>Nerdy Momo Cat</span>
          </div>
        </div>
        </div>
      </div>
    </div>
</div>
I like this, 3 part linear gradient
<div
  style={{
    height: '100%',
    width: '100%',
    display: 'flex',
    flexDirection: 'column',
    alignItems: 'center',
    justifyContent: 'center',
    fontSize: 32,
    fontWeight: 600,
    backgroundColor: 'white'
  }}
>
   <img src="https://maggieappleton.com/images/posts/squish-structure/sms2.66.jpeg" style={{position: 'absolute', // Absolutely positioned
      top: 0,
      left: 0,
      height: '100%',
      width: '100%',
maskImage: 'linear-gradient(to bottom, rgba(255, 255, 255, 0.15) 25%, rgba(255, 255, 255, 0.15) 25%, rgba(255, 255, 255, 0.3) 90%, rgba(255, 255, 255, 0.1) 90%)',
objectFit: 'cover'
    }} />

<div
      style={{
        padding: "20px",
        display: "flex",
        width: "100%",
        height: "100%",
        justifyContent: "center",
        alignItem: "stretch",
      }}
    >

      <div
        style={{
          display: "flex",
          flexDirection: "row",
          justifyContent: "space-between",
          border: "1px solid #374151",
          boxShadow: "5px 5px 0px #374151",
          width: "100%",
          height: "100%",
          padding: "10px",
        }}
      >

      <div style={{display:"flex", flexDirection:"column", flex: 1,}}>
        <div
          style={{
            fontSize: "px",
            fontWeight: "900",
            lineHeight: "3rem",
            padding: "10px 0 50px 0",
            color: "#374151",
            flex: 1,
            display: "flex",
            fontFamily: "PlusJakartaSans",
          }}
        >
          Demo New Page: Trying to get Astro+Notion working how I want to
        </div>
        <div
          style={{
            fontSize: "16px",
            fontWeight: "900",
            color: "#374151",
            display: "flex",
            flexDirection: "row",
            justifyContent: "space-between",
            alignItems: "center",
          }}
        >
          <div>
            DATE HERE
          </div>
          <div style={{ display: "flex", alignItems: "center" }}>
            <span style={{ marginRight: "16px" }}>Nerdy Momo Cat</span>
          </div>
        </div>
        </div>
      </div>
    </div>
</div>

📝 Using sharp instead of resvg because that is what the blogpost above uses and I ain’t figuring it out myself

Converting to tailwind satori format because uh, that is what cactus uses and I do not have enough brain or brainpower to figure it out.

Lol! satori does not support obejctFit in tailwind 🤣

Apparently, no conversion NEEDED. Why are these frameworks so complicated smh?

Alright, here is every template
const markup_img_bg = (title: string, pubDate: string, img_url: string) =>
  html`<div
  style="height: 100%; width: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; font-size: 32px; font-weight: 700; background-color: white; font-family: JetBrainsMono;"
>
   <img src="${img_url}"
        style="position: absolute; top: 0; left: 0; height: 100%; width: 100%; mask-image: linear-gradient(to bottom, rgba(255, 255, 255, 0.15) 25%, rgba(255, 255, 255, 0.15) 25%, rgba(255, 255, 255, 0.3) 90%, rgba(255, 255, 255, 0.1) 90%); object-fit: cover;"
   />

  <div
    style="padding: 20px; display: flex; width: 100%; height: 100%; justify-content: center; align-items: stretch;"
  >
    <div
      style="display: flex; flex-direction: row; justify-content: space-between; border: 1px solid #374151; box-shadow: 5px 5px 0px #374151; width: 100%; height: 100%; padding: 10px;"
    >
      <div style="display: flex; flex-direction: column; flex: 1;">
        <div
          style="font-size: px; font-weight: 700; line-height: 3rem; padding: 10px 0 50px 0; color: #374151; flex: 1; display: flex;"
        >
          ${title}
        </div>
        <div
          style="font-size: 16px; font-weight: 700; color: #374151; display: flex; flex-direction: row; justify-content: space-between; align-items: center;"
        >
          <div>
            ${pubDate}
          </div>
          <div style="display: flex; align-items: center;">
            <span style="margin-right: 16px;">${siteInfo.author}</span>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>`;

const markup_img_sq_with_desc = (title: string, pubDate: string, desc: string, img_url: string) =>
  html`<div
  style="height: 100%; width: 100%; display: flex; background-color: white; font-family: JetBrainsMono-Bold;"
>
  <div
      style="padding: 20px; display: flex; width: 100%; height: 100%; justify-content: center; align-items: stretch;"
  >
      <div
          style="display: flex; flex-direction: row; justify-content: space-between; border: 1px solid #374151; box-shadow: 5px 5px 0px #374151; width: 100%; height: 100%; padding: 10px;"
      >
          <div style="display: flex; flex: 1;">
              <img src="${img_url}"
                   style="width: 100%; height: 100%; object-fit: contain; object-position: center;" />
          </div>
          <div style="display: flex; flex-direction: column; flex: 1;">
              <div
                  style="font-size: 32px; font-weight: 700; line-height: 3rem; padding: 10px 0 50px 0; color: #374151; flex: 0.3; display: flex; font-family: monospace;"
              >
              ${title}
              </div>
              <div
                  style="font-size: 24px; font-weight: 700; line-height: 3rem; padding: 10px 0 50px 0; color: #374151; flex: 1; display: flex; font-family: monospace;"
              >
              ${desc}
              </div>
              <div
                  style="font-size: 16px; font-weight: 700; color: #374151; display: flex; flex-direction: row; justify-content: space-between; align-items: center;"
              >
                  <div>
                      ${pubDate}
                  </div>
                  <div style="display: flex; align-items: center;">
                      <span style="margin-right: 16px;">${siteInfo.author}</span>
                  </div>
              </div>
          </div>
      </div>
  </div>
</div>`;

const markup_img_sq_without_desc = (title: string, pubDate: string, img_url: string) =>
  html`<div
  style="height: 100%; width: 100%; display: flex; background-color: white; font-family: JetBrainsMono-Bold;"
>
  <div
      style="padding: 20px; display: flex; width: 100%; height: 100%; justify-content: center; align-items: stretch;"
  >
      <div
          style="display: flex; flex-direction: row; justify-content: space-between; border: 1px solid #374151; box-shadow: 5px 5px 0px #374151; width: 100%; height: 100%; padding: 10px;"
      >
          <div style="display: flex; flex: 1;">
              <img src="${img_url}"
                   style="width: 100%; height: 100%; object-fit: contain; object-position: center;" />
          </div>
          <div style="display: flex; flex-direction: column; flex: 1;">
              <div
                  style="font-size: 32px; font-weight: 700; line-height: 3rem; padding: 10px 0 50px 0; color: #374151; flex: 1; display: flex; font-family: monospace;"
              >
              ${title}
              </div>
              <div
                  style="font-size: 16px; font-weight: 700; color: #374151; display: flex; flex-direction: row; justify-content: space-between; align-items: center;"
              >
                  <div>
                      ${pubDate}
                  </div>
                  <div style="display: flex; align-items: center;">
                      <span style="margin-right: 16px;">${siteInfo.author}</span>
                  </div>
              </div>
          </div>
      </div>
  </div>
</div>`;

const markup_img_none_with_desc = (title: string, pubDate: string, desc: string) =>
  html`<div style="height: 100%; width: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; font-size: 32px; font-weight: 700; background-color: white; background-image: radial-gradient(circle at 25px 25px, lightgray 2%, transparent 0%), radial-gradient(circle at 75px 75px, lightgray 2%, transparent 0%); background-size: 100px 100px; font-family: JetBrainsMono-Bold;">

  <div style="padding: 20px; display: flex; width: 100%; height: 100%; justify-content: center; align-items: stretch;">

      <div style="display: flex; flex-direction: row; justify-content: space-between; border: 1px solid #374151; box-shadow: 5px 5px 0px #374151; width: 100%; height: 100%; padding: 10px;">

          <div style="display: flex; flex-direction: column; flex: 1;">
              <div style="font-size: 52px; font-weight: 700; line-height: 4rem; padding: 10px 50px; color: #374151; flex: 0.4; display: flex;">
                  ${title}
              </div>
              <div style="font-size: 30px; font-weight: 700; line-height: 4rem; padding: 10px 50px; color: #374151; flex: 1; display: flex;">
                  ${desc}
              </div>
              <div style="font-size: 24px; font-weight: 700; color: #374151; display: flex; flex-direction: row; justify-content: space-between; align-items: center; padding: 10px;">
                  <div>
                      ${pubDate}
                  </div>
                  <div style="display: flex; align-items: center;">
                      <span style="margin-right: 16px;">Nerdy Momo Cat</span>
                  </div>
              </div>
          </div>
      </div>
  </div>
</div>
`;

const markup_img_none_without_desc = (title: string, pubDate: string) =>
  html`<div style="height: 100%; width: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; font-size: 32px; font-weight: 700; background-color: white; background-image: radial-gradient(circle at 25px 25px, lightgray 2%, transparent 0%), radial-gradient(circle at 75px 75px, lightgray 2%, transparent 0%); background-size: 100px 100px; font-family: JetBrainsMono-Bold;">

  <div style="padding: 20px; display: flex; width: 100%; height: 100%; justify-content: center; align-items: stretch;">

      <div style="display: flex; flex-direction: row; justify-content: space-between; border: 1px solid #374151; box-shadow: 5px 5px 0px #374151; width: 100%; height: 100%; padding: 10px;">

          <div style="display: flex; flex-direction: column; flex: 1;">
              <div style="font-size: 52px; font-weight: 700; line-height: 4rem; padding: 10px 50px; color: #374151; flex: 1; display: flex;">
                  ${title}
              </div>
              <div style="font-size: 24px; font-weight: 700; color: #374151; display: flex; flex-direction: row; justify-content: space-between; align-items: center; padding: 10px;">
                  <div>
                      ${pubDate}
                  </div>
                  <div style="display: flex; align-items: center;">
                      <span style="margin-right: 16px;">Nerdy Momo Cat</span>
                  </div>
              </div>
          </div>
      </div>
  </div>
</div>
`;

I need to fix:

create different kinds of og images based on featured image
og images for non-post pages
Processing dump
  1. The only place meta property is set is in BaseHead.astro <meta property="og:image" content={socialImageURL} />

    There is an indication in Layout.astro, but remember that is just copied from otoyo’s blog for reference, and not actually used

    Now we have 2 things:

    const { title, description, ogImage, articleDate } = Astro.props;

    and

    const socialImageURL = new URL(ogImage ? ogImage : "/social-card.png", Astro.url).href;

  2. The only place BaseHead is imported is Base.Astro by import BaseHead from "@/components/BaseHead";
    const {
    	meta: { title = siteInfo.title, description = siteInfo.description, ogImage, articleDate },
    } = Astro.props;

    <BaseHead title={title} description={description} ogImage={ogImage} articleDate={articleDate} />

  3. Finally Base is imported at multiple places
    BlogPost.Astro
    const { post, ogImage, headings } = Astro.props;
    const socialImage = ogImage ?? `/og-image/${post.Slug}.png`;
    <BaseLayout
    	meta={{ title: post.Title, description: post.Excerpt, articleDate, ogImage: socialImage }}
    >
    Now, in src/pages/posts/[slug].astro
    import PostLayout from "@/layouts/BlogPost.astro";
    
    export async function getStaticPaths() {
    	const posts = await getAllPosts();
    
    	return posts.map((post) => ({
    		params: { slug: post.Slug },
    		props: { post },
    	}));
    }
    interface Props {
    	post: Post;
    }
    const { post } = Astro.props;
    
    if (!post) {
    	throw new Error("Post not found. slug: ${slug}");
    }
    
    let ogImage;
    if (post.FeaturedImage) {
    	ogImage = new URL(filePath(new URL(post.FeaturedImage.Url)), Astro.site).toString();
    }
    
    <PostLayout post={post} ogImage={ogImage} headings={headings}>

    It is not possible to know size of the image at build time, so I cannot do the square, non-square thing. I am just gonna put it as an option!

    [..page].astro in src/pages
    export async function getStaticPaths() {
    	const pages = await getPages();
    
    	return pages.map((page) => ({
    		params: { page: page.Slug === siteInfo.homePageSlug ? undefined : `/${page.Slug}` },
    		props: { page },
    	}));
    }
    
    interface Props {
    	page: Post;
    }
    
    const { page: slug } = Astro.params;
    const { page } = Astro.props;
    <PageLayout meta={{ title: page.Title || siteInfo.title }}>
    404.astro in src/pages
    const meta = {
    	title: "Oops! You found a missing page!",
    	description: "Oops! It looks like this page is lost in space!",
    };
    ---
    
    <PageLayout meta={meta}>
    [..page].astro in src/pages/posts
    const meta = {
    	title: "Posts",
    	description: "Ready my collection of posts and the things that interest me",
    };
    
    <PageLayout meta={meta}>
    [..page].astro in src/pages/posts/collection/[collection]
    const meta = {
    	title: `${collection}`,
    	description: "Read my collection of posts.",
    };
    
    <PageLayout meta={meta}>
    index.astro in src/pages/tags
    const meta = {
    	title: "All Tags",
    	description: "A list of all the topics I've written about in my posts",
    };
    ---
    
    <PageLayout meta={meta}>
    [..page].astro in src/pages/tags/[tag]/[...page].astro
    const meta = {
    	title: `Tag: ${tag}`,
    	description: `View all posts with the tag - ${tag}`,
    };
    
    <PageLayout meta={meta}>
Where are these download files?
export async function downloadFile(url: URL) {
  let res!: AxiosResponse;
  try {
    res = await axios({
      method: "get",
      url: url.toString(),
      timeout: REQUEST_TIMEOUT_MS,
      responseType: "stream",
    });
  } catch (err) {
    console.log(err);
    return Promise.resolve();
  }

  if (!res || res.status != 200) {
    console.log(res);
    return Promise.resolve();
  }

  const BASE_DIR = "./public/notion/";
  if (!fs.existsSync(BASE_DIR)) {
    fs.mkdirSync(BASE_DIR);
  }

  const dir = BASE_DIR + url.pathname.split("/").slice(-2)[0];
  if (!fs.existsSync(dir)) {
    fs.mkdirSync(dir);
  }

  const filename = decodeURIComponent(url.pathname.split("/").slice(-1)[0]);
  const filepath = `${dir}/${filename}`;

  const writeStream = fs.createWriteStream(filepath);
  const rotate = sharp().rotate();

  let stream = res.data;

  if (res.headers["content-type"] === "image/jpeg") {
    stream = stream.pipe(rotate);
  }
  stream.pipe(new ExifTransformer()).pipe(writeStream);
}

Lol! We are back to the issue of image not being generated, and satori needs absolute path, and base64 just refuses to work with larger images?

Thought — we are eventually generating a png anyway. If the data has not expired — we can maybe just use that. Expiry duration seems to be an hour

.webp does not work for this, but that is okay. just use png or jpeg.

Example of new URL(filePath(new URL(post.FeaturedImage.Url)), Astro.site).toString(); and post.FeaturedImage.Url

Dumped code of using sharp and buffer etc in case I need it later
var dimensions = sizeOf('public/notion/90518bf5-b2a3-49ce-af8d-5fa59d9e11f4/ai-forest2x.webp');
  console.log(new URL(post.FeaturedImage.Url)).toString();
  // console.log(dimensions.width, dimensions.height);

  const buffimg = await sharp("public/notion/90518bf5-b2a3-49ce-af8d-5fa59d9e11f4/ai-forest2x.webp").toBuffer();
  // console.log(`data:image/png;base64,${buffimg.toString('base64')}`);
  // console.log(metadata);
  // let chosen_markup;
  // let chosen_markup = markup_img_none_without_desc(title, postDate);
  let chosen_markup = markup_img_sq_without_desc(title, postDate, "data:image/png;base64, iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==");
  console.log(chosen_markup);
http://localhost:4321/notion/d0d55339-5e7e-4aae-8fea-af6b97142bf9/1548870028.png
https://prod-files-secure.s3.us-west-2.amazonaws.com/6083354a-c9d6-441d-886e-3fa82c5b48de/d0d55339-5e7e-4aae-8fea-af6b97142bf9/1548870028.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=AKIAT73L2G45HZZMZUHI%2F20231126%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20231126T024537Z&X-Amz-Expires=3600&X-Amz-Signature=60184f099b79dfa496ebc2d27c6cf58ffea13e8f98e0c28352e60c06a98346eb&X-Amz-SignedHeaders=host&x-id=GetObject

So image URLs here have & which satori very nicely (NOT!) escapes leading to invalid url for image. I just…cannot stop laughing. It has been 10 hours.

Nov 26, 2023, 06:31 AM : got it to work for all posts. It might fail if it does not understand width/height, but I think that is okay?

Lol, I added this for my sanity. GPT is awesome for such things

let svg;
  try {
    svg = await satori(chosen_markup, ogOptions);
  } catch (error) {
    console.error("Error in satori:", error);
    // Fallback to a basic markup if satori fails
    const fallback_markup = post?.Excerpt ? obj_img_none_with_desc(title, postDate, post?.Excerpt) : obj_img_none_without_desc(title, postDate);
    svg = await satori(fallback_markup, ogOptions);
  }


Look at the cache stuff in otoyo’s post. Maybe it is just for cloudfare? Idk!
THEME AND FONTS!
Logo to database icon!
Set favicon!
Side note, apparently if it is a video embed but not youtube, it is not rendered? Maybe call the generic embed in that case?
Code here
videoUrl &&
			(url ? (
				isYouTubeURL(url) ? (
					<iframe
          class="w-full h-[340px] max-w-sm:h-[220px]"
						src={`https://www.youtube.com/embed/${parseYouTubeVideoId(url)}`}
						title="YouTube video player"
						allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
						allowfullscreen
					/>
				) : null
Caption is not rendering links because of this, need to shift it to a rich text output. Caption cannot have children, I think! Yes, they cannot!
{
	richTexts.length > 0 && richTexts[0].Text.Content && (
		<div class="caption pt-1 text-sm">{richTexts[0].Text.Content}</div>
	)
}
Heading toggle clashes with the # sign, what is a good idea here? maybe especially change margin for those??
Nov 26, 2023
Do not show pages with slug starting with underscore in
collection index page,
posts page,
tag page,
rss,
sitemap
links to main pages and pages as URL seems to be broken?
Logo to database icon!
Side note, apparently if it is a video embed but not youtube, it is not rendered? Maybe call the generic embed in that case?
Code here
videoUrl &&
			(url ? (
				isYouTubeURL(url) ? (
					<iframe
          class="w-full h-[340px] max-w-sm:h-[220px]"
						src={`https://www.youtube.com/embed/${parseYouTubeVideoId(url)}`}
						title="YouTube video player"
						allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
						allowfullscreen
					/>
				) : null
Why the fudge does it keep scrolling to tiktok man?
I am having scroll issues with it and can’t set muted and sometimes a scrollbar shows up in iframe. I just used muted on blockquote
<lite-tiktok videoid={videoId} autoload></lite-tiktok>;

<script>
	//Copied from https://github.com/justinribeiro/lite-tiktok because I could not get import to work for some stupid reason
	/**
	 *
	 * A web component that lazy loads TikTok embeds. Currently experimental and a work in progress.
	 *
	 * A less interesting variation of lite-youtube (https://github.com/justinribeiro/lite-youtube)
	 *
	 */
	export class LiteTTEmbed extends HTMLElement {
		shadowRoot!: ShadowRoot;
		private domRefFrame!: HTMLDivElement;
		private domRefImg!: {
			fallback: HTMLImageElement;
			webp: HTMLSourceElement;
			jpeg: HTMLSourceElement;
		};
		private domRefPlayButton!: HTMLButtonElement;
		private static isPreconnected = false;
		private isIframeLoaded = false;

		constructor() {
			super();
			this.setupDom();
		}

		static get observedAttributes(): string[] {
			return ["videoid", "playlistid"];
		}

		connectedCallback(): void {
			this.addEventListener("pointerover", LiteTTEmbed.warmConnections, {
				once: true,
			});

			this.addEventListener("click", () => this.addIframe());
		}

		get videoId(): string {
			return encodeURIComponent(this.getAttribute("videoid") || "");
		}

		set videoId(id: string) {
			this.setAttribute("videoid", id);
		}

		get autoLoad(): boolean {
			return this.hasAttribute("autoload");
		}

		set __data(obj: any) {
			this.__data = obj;
		}

		get __data(): any {
			return this.__data;
		}

		/**
		 * Define our shadowDOM for the component
		 */
		private setupDom(): void {
			const shadowDom = this.attachShadow({ mode: "open" });
			shadowDom.innerHTML = `
      <style>
        :host {
          contain: content;
          display: block;
          position: relative;
          width: 100%;
          height: 735px;
        }

        #frame, iframe {
          position: absolute;
          width: 100%;
          height: 100%;
          left: 0;
        }

        #frame {
          cursor: pointer;
        }

        #fake {
          display: flex;
          position: relative;
          height: 100%;
          justify-content: center;
          background: #000;
          width: 80%;
          margin: auto;
        }

        #logo {
          position: absolute;
          width: 48px;
          height: 48px;
          margin: 1rem;
          right: 0;
        }

        #fallbackPlaceholder {
          height: 100%;
          position: fixed;
          top: 50%;
          left: 50%;
          transform: translate(-50%, -50%);
        }

        #playButton {
          width: 150px;
          height: 150px;
          background-color: transparent;
          z-index: 1;
          border: 0;
        }

        #playButton:before {
          content: '';
          border-style: solid;
          border-width: 22px 0 22px 40px;
          border-color: transparent transparent transparent #fff;
        }

        #playButton,
        #playButton:before {
          position: absolute;
          top: 50%;
          left: 50%;
          transform: translate3d(-50%, -50%, 0);
          cursor: inherit;
        }

        /* Post-click styles */
        .activated {
          cursor: unset;
        }

        #frame.activated::before,
        #frame.activated > #fake {
          display: none;
        }
      </style>
      <div id="frame">
        <div id="fake">
          <picture>
            <source id="jpegPlaceholder" type="image/jpeg">
            <img id="fallbackPlaceholder" referrerpolicy="origin" loading="lazy">
          </picture>
          <button id="playButton"></button>
          <svg id="logo" xmlns="http://www.w3.org/2000/svg"><path d="M19.8 19.8v-1.6L18 18a12.1 12.1 0 0 0-7 22 12 12 0 0 1 8.6-20.3Z" fill="#25F4EE"/><path d="M20 37.4c3 0 5.5-2.4 5.6-5.3V5.7h4.8L30.3 4h-6.6v26.4a5.5 5.5 0 0 1-8.1 4.7c1 1.4 2.6 2.3 4.4 2.3Zm19.4-22.7v-1.5c-1.8 0-3.5-.6-5-1.5a9 9 0 0 0 5 3Z" fill="#25F4EE"/><path d="M34.4 11.7a9.1 9.1 0 0 1-2.2-6h-1.8c.4 2.5 2 4.6 4 6ZM18.1 24.6c-3 0-5.6 2.5-5.6 5.6 0 2.1 1.3 4 3 4.9a5.5 5.5 0 0 1 6.2-8.5v-6.7l-1.7-.2h-.2v5.1l-1.7-.2Z" fill="#FE2C55"/><path d="M39.4 14.6v5.1c-3.4 0-6.6-1.1-9.2-3v13.5a12 12 0 0 1-19 10 12.1 12.1 0 0 0 21-8.3V18.5a16 16 0 0 0 9.1 3v-6.7c-.6 0-1.3 0-1.9-.2Z" fill="#FE2C55"/><path d="M30.3 30.2V16.8a15 15 0 0 0 9.1 3v-5.2c-2-.4-3.7-1.4-5-3a9.2 9.2 0 0 1-4-6h-4.8v26.5a5.5 5.5 0 0 1-10 3 5.5 5.5 0 0 1 4.2-10.2v-5.2a12.1 12.1 0 0 0-8.6 20.4 12 12 0 0 0 19-9.9Z" fill="#fff"/></svg>
        </div>
      </div>
    `;
			this.domRefFrame = shadowDom.querySelector<HTMLDivElement>("#frame")!;
			this.domRefImg = {
				fallback: shadowDom.querySelector("#fallbackPlaceholder")!,
				webp: shadowDom.querySelector("#webpPlaceholder")!,
				jpeg: shadowDom.querySelector("#jpegPlaceholder")!,
			};
			this.domRefPlayButton = shadowDom.querySelector("#playButton")!;
		}

		/**
		 * Parse our attributes and fire up some placeholders
		 */
		private async setupComponent(): Promise<void> {
			// Don't take the hit if the autoload is set because this is all just too
			// expensive given the base embeds weight
			if (!this.autoLoad) {
				const request = await fetch(
					`https://www.tiktok.com/oembed?url=https://www.tiktok.com/video/${this.videoId}`,
				);
				const data = await request.json();

				this.initImagePlaceholder(data);

				this.domRefPlayButton.setAttribute("aria-label", `Play: ${data.title}`);
				this.setAttribute("title", `Play: ${data.title}`);
			} else {
				this.initIntersectionObserver();
			}
		}

		/**
		 * Lifecycle method that we use to listen for attribute changes to period
		 * @param {*} name
		 * @param {*} oldVal
		 * @param {*} newVal
		 */
		attributeChangedCallback(name: string, oldVal: unknown, newVal: unknown): void {
			switch (name) {
				case "videoid": {
					if (oldVal !== newVal) {
						this.setupComponent();

						// if we have a previous iframe, remove it and the activated class
						if (this.domRefFrame.classList.contains("activated")) {
							this.domRefFrame.classList.remove("activated");
							this.shadowRoot.querySelector("iframe")!.remove();
							this.isIframeLoaded = false;
						}
					}
					break;
				}
				default:
					break;
			}
		}

		/**
		 * Inject the iframe into the component body
		 * @param {boolean} isIntersectionObserver
		 */
		private addIframe(isIntersectionObserver = false): void {
			if (!this.isIframeLoaded) {
				const iframeHTML = `
<iframe frameborder="0"
  sandbox="allow-popups allow-popups-to-escape-sandbox allow-scripts allow-top-navigation allow-same-origin"
  src="https://www.tiktok.com/embed/v2/${this.videoId}">
</iframe>
`;
				this.domRefFrame.insertAdjacentHTML("beforeend", iframeHTML);
				this.domRefFrame.classList.add("activated");
				this.isIframeLoaded = true;
			}
		}

		/**
		 * Setup the placeholder image for the component
		 */
		private initImagePlaceholder(data: any): void {
			this.domRefImg.jpeg.srcset = data.thumbnail_url;
			this.domRefImg.fallback.src = data.thumbnail_url;
			this.domRefImg.fallback.setAttribute("aria-label", `Play: ${data.title}`);
			this.domRefImg?.fallback?.setAttribute("alt", `Play: ${data.title}`);
		}

		/**
		 * Setup the Intersection Observer to load the iframe when scrolled into view
		 */
		private initIntersectionObserver(): void {
			const options = {
				root: null,
				rootMargin: "0px",
				threshold: 0,
			};

			const observer = new IntersectionObserver((entries, observer) => {
				entries.forEach((entry) => {
					if (entry.isIntersecting && !this.isIframeLoaded) {
						LiteTTEmbed.warmConnections();
						this.addIframe(true);
						observer.unobserve(this);
					}
				});
			}, options);

			observer.observe(this);
		}

		/**
		 * Add a <link rel={preload | preconnect} ...> to the head
		 * @param {string} kind
		 * @param {string} url
		 * @param {string} as
		 */
		private static addPrefetch(kind: string, url: string): void {
			const linkElem = document.createElement("link");
			linkElem.rel = kind;
			linkElem.href = url;
			linkElem.crossOrigin = "true";
			document.head.append(linkElem);
		}

		/**
		 * Begin preconnecting to warm up the iframe load Since the embed's network
		 * requests load within its iframe, preload/prefetch'ing them outside the
		 * iframe will only cause double-downloads. So, the best we can do is warm up
		 * a few connections to origins that are in the critical path.
		 *
		 * Maybe `<link rel=preload as=document>` would work, but it's unsupported:
		 * http://crbug.com/593267 But TBH, I don't think it'll happen soon with Site
		 * Isolation and split caches adding serious complexity.
		 */
		private static warmConnections(): void {
			if (LiteTTEmbed.isPreconnected) return;

			LiteTTEmbed.addPrefetch("preconnect", "https://www.tiktok.com");
			LiteTTEmbed.addPrefetch("preconnect", "https://mcs.us.tiktok.com");
			LiteTTEmbed.addPrefetch("preconnect", "https://mon.us.tiktokv.com");

			LiteTTEmbed.isPreconnected = true;
		}
	}
	// Register custom element
	customElements.define("lite-tiktok", LiteTTEmbed);
</script>
Nov 26, 2023, 10:42 PM
how do i use webmentions? #LATER
Look at the cache stuff in otoyo’s post. Maybe it is just for cloudfare? Idk! #LATER
Set favicon!
Why does otoyo add everything to public and not to src? Maybe adding to src will help with optimization?
This is gonna be difficult because while Image has bundling things, everything else does not? and because everything else does not, it needs to be referenced using public anyway. So, I can technically do this where I route downloaded images, videos, audio and files to different places and import using that, but that is too much work and I ain’t doing that!
Trying to optimize just images
Original
export async function downloadFile(url: URL) {
  let res!: AxiosResponse;
  try {
    res = await axios({
      method: "get",
      url: url.toString(),
      timeout: REQUEST_TIMEOUT_MS,
      responseType: "stream",
    });
  } catch (err) {
    console.log(err);
    return Promise.resolve();
  }

  if (!res || res.status != 200) {
    console.log(res);
    return Promise.resolve();
  }

  const BASE_DIR = "./public/notion/";
  // const BASE_DIR = "./src/notion-assets/";
  if (!fs.existsSync(BASE_DIR)) {
    fs.mkdirSync(BASE_DIR);
  }

  const dir = BASE_DIR + url.pathname.split("/").slice(-2)[0];
  if (!fs.existsSync(dir)) {
    fs.mkdirSync(dir);
  }

  const filename = decodeURIComponent(url.pathname.split("/").slice(-1)[0]);
  const filepath = `${dir}/${filename}`;

  const writeStream = fs.createWriteStream(filepath);
  const rotate = sharp().rotate();

  let stream = res.data;

  if (res.headers["content-type"] === "image/jpeg") {
    stream = stream.pipe(rotate);
  }
  stream.pipe(new ExifTransformer()).pipe(writeStream);
}
New
export async function downloadFile(url: URL, optimize_img = true) {
  let res!: AxiosResponse;
  try {
    res = await axios({
      method: "get",
      url: url.toString(),
      timeout: REQUEST_TIMEOUT_MS,
      responseType: "stream",
    });
  } catch (err) {
    console.log(err);
    return Promise.resolve();
  }

  if (!res || res.status != 200) {
    console.log(res);
    return Promise.resolve();
  }

  const BASE_DIR = "./public/notion/";
  if (!fs.existsSync(BASE_DIR)) {
    fs.mkdirSync(BASE_DIR);
  }

  const dir = BASE_DIR + url.pathname.split("/").slice(-2)[0];
  if (!fs.existsSync(dir)) {
    fs.mkdirSync(dir);
  }

  const filename = decodeURIComponent(url.pathname.split("/").slice(-1)[0]);
  const filepath = `${dir}/${filename}`;

  let stream = res.data;
  if (res.headers["content-type"] === "image/jpeg") {
    stream = stream.pipe(sharp().rotate());
  }

  const isImage = res.headers["content-type"]?.startsWith("image/");
  if (isImage && optimize_img) {
    // Process and write only the optimized WebP image
    // const webpPath = `${dir}/${filename.split('.')[0]}.webp`;
    const webpPath = `${dir}/${filename.substring(0, filename.lastIndexOf('.'))}.webp`
    stream.pipe(sharp()
      // .resize({ width: 1024 }) // Adjust the size as needed for "medium"
      .webp({ quality: 80 })) // Adjust quality as needed
      .toFile(webpPath)
      .catch(err => {
        console.error('Error processing image:', err);
      });
  } else {
    // Original behavior for non-image files or when not optimizing
    const writeStream = fs.createWriteStream(filepath);
    stream.pipe(new ExifTransformer()).pipe(writeStream);
  }
}
Headings as nested children do not show up in ToC
Process

So, blocks show up as

{
    Id: 'f042d61d-6289-49b0-9276-945047b8561f',
    Type: 'column_list',
    HasChildren: true,
    ColumnList: { Columns: [Array] }
  },
{
    Id: '44a84a9a-60e6-443d-8434-e5a9d43ff06a',
    Type: 'numbered_list_item',
    HasChildren: true,
    NumberedListItem: { RichTexts: [Array], Color: 'default', Children: [Array] }
  },

so, I will need to flatten these I guess?

I can do something like this but I am not sure I want it. Plan cancelled (even Notion does not do it!)

export function buildHeadings(blocks: Block[]): Heading[] {
  // Helper function to flatten blocks recursively
  function flattenBlocks(blocks: Block[]): Block[] {
    return blocks.reduce((acc, block) => {
      acc.push(block);

      // Handle column_list blocks
      if (block.Type === 'column_list' && block.ColumnList) {
        block.ColumnList.Columns.forEach(column => {
          if (column.HasChildren && column.Children) {
            acc.push(...flattenBlocks(column.Children));
          }
        });
      }
      // Recursively flatten other blocks except table_row
      else if (block.Type !== 'table_row' && block.HasChildren && (block.Paragraph || block.Heading1 || block.Heading2 || block.Heading3 || block.BulletedListItem || block.NumberedListItem || block.ToDo || block.Quote || block.Callout || block.SyncedBlock || block.Toggle).Children) {
        acc.push(...flattenBlocks((block.Paragraph || block.Heading1 || block.Heading2 || block.Heading3 || block.BulletedListItem || block.NumberedListItem || block.ToDo || block.Quote || block.Callout || block.SyncedBlock || block.Toggle).Children));
      }

      return acc;
    }, [] as Block[]);
  }

  // Flatten the block structure
  const allBlocks = flattenBlocks(blocks);

  // Filter for heading blocks
  return allBlocks.filter(block => HEADING_BLOCKS.includes(block.Type)).map(cleanHeading);
}
Links on this page #Medium Term LATER Might need some flattening like this Show information for the linked content . And see this is a good use of not having all headings show up in ToC because I can use them for linking internally while still not cluttering stuff! Win Win!
Add any html/md content to posts. I am not sure how this works and I’ll need to figure out a way but that is too much work man.
Only show last updated if it is not same
THEME AND FONTS AND CSS!!
Replaced 404 page from here
Colors
Original tailwind.config.cs for safety purposes
import type { Config } from "tailwindcss";
import { fontFamily } from "tailwindcss/defaultTheme";
import plugin from "tailwindcss/plugin";
export default {
  content: ["./src/**/*.{astro,html,js,jsx,md,svelte,ts,tsx,vue}"],
  darkMode: "class",
  corePlugins: {
    // disable aspect ratio as per docs -> @tailwindcss/aspect-ratio
    aspectRatio: false,
    // disable some core plugins as they are included in the css, even when unused
    touchAction: false,
    ringOffsetWidth: false,
    ringOffsetColor: false,
    scrollSnapType: false,
    borderOpacity: false,
    textOpacity: false,
    fontVariantNumeric: false,
  },
  theme: {
    extend: {
      colors: {
        bgColor: "hsl(var(--theme-bg) / <alpha-value>)",
        textColor: "hsl(var(--theme-text) / <alpha-value>)",
        link: "hsl(var(--theme-link) / <alpha-value>)",
        accent: "hsl(var(--theme-accent) / <alpha-value>)",
        "accent-2": "hsl(var(--theme-accent-2) / <alpha-value>)",
        quote: "hsl(var(--theme-quote) / <alpha-value>)",
      },
      fontFamily: {
        // Add any custom fonts here
        sans: [...fontFamily.sans],
        serif: [...fontFamily.serif],
        mono: [...fontFamily.mono]
      },
      transitionProperty: {
        height: "height",
      },
 eslint-disable-next-line @typescript-eslint/ban-ts-comment
      @ts-ignore
      Remove above once tailwindcss exposes theme type
      typography: (theme) => ({
        cactus: {
          css: {
            "--tw-prose-body": theme("colors.textColor / 1"),
            "--tw-prose-headings": theme("colors.accent-2 / 1"),
            "--tw-prose-links": theme("colors.textColor / 1"),
            "--tw-prose-bold": theme("colors.textColor / 1"),
            "--tw-prose-bullets": theme("colors.textColor / 1"),
            "--tw-prose-quotes": theme("colors.quote / 1"),
            "--tw-prose-code": theme("colors.textColor / 1"),
            "--tw-prose-hr": "0.5px dashed #666",
            "--tw-prose-th-borders": "#666",
          },
        },
        DEFAULT: {
          css: {
            a: {
              "@apply cactus-link no-underline": "",
            },
            strong: {
              fontWeight: "700",
            },
            code: {
              border: "1px dotted #666",
              borderRadius: "2px",
            },
            blockquote: {
              borderLeftWidth: "0",
            },
            hr: {
              borderTopStyle: "dashed",
            },
            thead: {
              borderBottomWidth: "none",
            },
            "thead th": {
              fontWeight: "700",
              borderBottom: "1px dashed #666",
            },
            "tbody tr": {
              borderBottomWidth: "none",
            },
            tfoot: {
              borderTop: "1px dashed #666",
            },
            sup: {
              "@apply ms-0.5": "",
              a: {
                "@apply bg-none": "",
                "&:hover": {
                  "@apply text-link no-underline bg-none": "",
                },
                "&:before": {
                  content: "'['",
                },
                "&:after": {
                  content: "']'",
                },
              },
            },
          },
        },
        sm: {
          css: {
            code: {
              fontSize: theme("fontSize.sm")[0],
              fontWeight: "400",
            },
          },
        },
      }),
    },
  },
  plugins: [
    // require("@tailwindcss/typography"),
    require("@tailwindcss/aspect-ratio"),
    plugin(function ({ addComponents }) {
      addComponents({
        ".cactus-link": {
          "@apply bg-[size:100%_6px] bg-bottom bg-repeat-x": {},
          backgroundImage:
            "linear-gradient(transparent,transparent 5px,hsl(var(--theme-text)) 5px,hsl(var(--theme-text)))",
          "&:hover": {
            backgroundImage:
              "linear-gradient(transparent,transparent 4px,hsl(var(--theme-link)) 4px,hsl(var(--theme-link)))",
          },
        },
        ".title": {
          "@apply text-2xl font-semibold text-accent-2": {},
        },
      });
    }),
  ],
} satisfies Config;
Original global.css for safety purposes
@tailwind base;
@tailwind components;
@tailwind utilities;

@layer base {
  :root {
    color-scheme: light;
    /* https://tailwindcss.com/docs/customizing-colors#using-css-variables */
    --theme-bg: 0deg 0% 98%;
    --theme-link: 172deg 18% 41%;
    --theme-text: 203deg 11% 15%;
    --theme-accent: 351deg 66% 48%;
    --theme-accent-2: 0deg 0% 7%;
    --theme-quote: 351deg 66% 48%;
  }

  :root.dark {
    color-scheme: dark;
    --theme-bg: 210deg 6% 12%;
    --theme-link: 330deg 49% 67%;
    --theme-text: 220deg 3% 79%;
    --theme-accent: 159deg 64% 45%;
    --theme-accent-2: 0deg 0% 93%;
    --theme-quote: 102deg 100% 86%;
  }

  html {
    @apply scroll-smooth;
  }

  html body {
    @apply mx-auto flex min-h-screen max-w-3xl flex-col bg-bgColor px-8 pt-8 text-textColor antialiased overflow-x-hidden;
  }
}
Process

Focussing here

extend: {
      colors: {
        bgColor: "hsl(var(--theme-bg) / <alpha-value>)",
        textColor: "hsl(var(--theme-text) / <alpha-value>)",
        link: "hsl(var(--theme-link) / <alpha-value>)",
        accent: "hsl(var(--theme-accent) / <alpha-value>)",
        "accent-2": "hsl(var(--theme-accent-2) / <alpha-value>)",
        quote: "hsl(var(--theme-quote) / <alpha-value>)",
      },
      fontFamily: {
        // Add any custom fonts here
        sans: [...fontFamily.sans],
        serif: [...fontFamily.serif],
        mono: [...fontFamily.mono]
      },
Why are theses themed this way? I am losing hope for my method
<style is:global>
	:root {
		--pagefind-ui-font: inherit;
	}

	#cactus__search .pagefind-ui__search-clear {
		width: calc(60px * var(--pagefind-ui-scale));
		padding: 0;
		background-color: transparent;
		overflow: hidden;
	}
	#cactus__search .pagefind-ui__search-clear:focus {
		outline: 1px solid theme("colors.accent-2.DEFAULT");
	}
	#cactus__search .pagefind-ui__search-clear::before {
		content: "";
		-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke-width='1.5' stroke='currentColor' %3E%3Cpath stroke-linecap='round' stroke-linejoin='round' d='M6 18L18 6M6 6l12 12'%3E%3C/path%3E%3C/svg%3E")
			center / 60% no-repeat;
		mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke-width='1.5' stroke='currentColor' %3E%3Cpath stroke-linecap='round' stroke-linejoin='round' d='M6 18L18 6M6 6l12 12'%3E%3C/path%3E%3C/svg%3E")
			center / 60% no-repeat;
		background-color: theme("colors.accent.DEFAULT");
		display: block;
		width: 100%;
		height: 100%;
	}

	#cactus__search .pagefind-ui__result {
		border: 0;
	}

	#cactus__search .pagefind-ui__result-link {
		background-size: 100% 6px;
		background-position: bottom;
		background-repeat: repeat-x;
		background-image: linear-gradient(
			transparent,
			transparent 5px,
			theme("colors.textColor.DEFAULT") 5px,
			theme("colors.textColor.DEFAULT")
		);
	}

	#cactus__search .pagefind-ui__result-link:hover {
		text-decoration: none;
		background-image: linear-gradient(
			transparent,
			transparent 4px,
			theme("colors.link.DEFAULT") 4px,
			theme("colors.link.DEFAULT")
		);
	}

	#cactus__search mark {
		color: theme("colors.quote.DEFAULT");
		background-color: transparent;
		font-weight: 600;
	}
</style>

<style>
	#cactus__search {
		--pagefind-ui-primary: theme("colors.accent.DEFAULT");
		--pagefind-ui-text: theme("colors.textColor.DEFAULT");
		--pagefind-ui-background: theme("colors.bgColor.DEFAULT");
		--pagefind-ui-border: theme("colors.zinc.400");
		--pagefind-ui-border-width: 1px;
	}
</style>
Especially because, there is this too!
const colorThemeMetaTag = document.querySelector("meta[name='theme-color']");
		document.documentElement.classList.toggle("dark", newTheme === "dark");
		const bgColour = getComputedStyle(document.body).getPropertyValue("--theme-bg");

		colorThemeMetaTag.setAttribute("content", `hsl(${bgColour})`);
		if (typeof localStorage !== "undefined") {
			localStorage.setItem("theme", newTheme);
		}
	}
Saving current mod if I change my mind
colors: {
        bgColor:
        {
          DEFAULT: "hsl(0deg 0% 98% / <alpha-value>)",
          "dark": "hsl(210deg 6% 12% / <alpha-value>)"
        },
        textColor:
        {
          DEFAULT: "hsl(var(--theme-text) / <alpha-value>)",
          "dark": "hsl(var(--theme-text) / <alpha-value>)"
        },
        link:
        {
          DEFAULT: "hsl(var(--theme-link) / <alpha-value>)",
          "dark": "hsl(var(--theme-link) / <alpha-value>)"
        },
        accent:
        {
          DEFAULT: "hsl(var(--theme-accent) / <alpha-value>)",
          "dark": "hsl(var(--theme-accent) / <alpha-value>)"
        },
        "accent-2":
        {
          DEFAULT: "hsl(var(--theme-accent-2) / <alpha-value>)",
          "dark": "hsl(var(--theme-accent-2) / <alpha-value>)"
        },
        "quote":
        {
          DEFAULT: "hsl(var(--theme-quote) / <alpha-value>)",
          "dark": "hsl(var(--theme-quote) / <alpha-value>)"
        }
      },
Fonts
From lillian weng’s blog

font-family: -apple-system,BlinkMacSystemFont,segoe ui,Roboto,Oxygen,Ubuntu,Cantarell,open sans,helvetica neue,sans-serif;

Header expands completely?
Process

Somehow, the sticky header part is not working, will look later

Is it even adding much benefit then? I do not like the part where it shifts layout rather than popup 😕

Code if I need again (Search terms, astro, navbar, astro-navbar)
---
import { Dropdown as DropdownContainer, DropdownItems } from "astro-navbar";
const { title, lastItem, children } = Astro.props;
---

<li class="relative">
  <DropdownContainer class="group">
    <button
      class="flex items-center gap-1 w-full lg:w-auto lg:px-3 py-2 text-gray-600 hover:text-gray-900">
      <span>{title}</span>
      <svg
        xmlns="http://www.w3.org/2000/svg"
        fill="none"
        viewBox="0 0 24 24"
        stroke-width="3"
        stroke="currentColor"
        class="w-3 h-3 mt-0.5 group-open:rotate-180">
        <path
          stroke-linecap="round"
          stroke-linejoin="round"
          d="M19.5 8.25l-7.5 7.5-7.5-7.5"></path>
      </svg>
    </button>
    <DropdownItems>
      <div
        class:list={[
          "lg:absolute w-full lg:w-48 z-10",
          lastItem
            ? "lg:right-0 origin-top-right"
            : "lg:left-0 origin-top-left",
        ]}>
        <div
          class="px-3 lg:py-2 lg:bg-white lg:rounded-md lg:shadow lg:border flex flex-col">
          {
            children.map((item) => (
              <a
                href={item.path}
                class="py-1 text-gray-600 hover:text-gray-900">
                {item.title}
              </a>
            ))
          }
        </div>
      </div>
    </DropdownItems>
  </DropdownContainer>
</li>
---
interface Props {
  href: string;
  size?: "md" | "lg";
  block?: boolean;
  style?: "outline" | "primary" | "inverted" | "muted";
  class?: string;
  [x: string]: any;
}

const {
  href,
  block,
  size = "lg",
  style = "primary",
  class: className,
  ...rest
} = Astro.props;

const sizes = {
  lg: "px-5 py-2.5",
  md: "px-4 py-2",
};

const styles = {
  outline: "bg-white border-2 border-black hover:bg-gray-100 text-black ",
  primary: "bg-black text-white hover:bg-gray-800  border-2 border-transparent",
  inverted: "bg-white text-black   border-2 border-transparent",
  muted: "bg-gray-100 hover:bg-gray-200   border-2 border-transparent",
};
---

<a
  href={href}
  {...rest}
  class:list={[
    "rounded text-center transition focus-visible:ring-2 ring-offset-2 ring-gray-200",
    block && "w-full",
    sizes[size],
    styles[style],
    className,
  ]}
  ><slot />
</a>
---
import Link from "./link.astro";
import Dropdown from "./dropdown.astro";
import { Astronav, MenuItems, MenuIcon, StickyHeader } from "astro-navbar";

import ThemeToggle from "../ThemeToggle.astro";
import Search from "../Search.astro";
import { getMenu } from "@/utils";
import { siteInfo } from "@/site.config";
import { filePath } from "@/lib/blog-helpers";

const menuitems = await getMenu();

const url = new URL(Astro.request.url);

let customIconURL: string = "";
if (siteInfo.logo && siteInfo.logo.Type === "file") {
	try {
		customIconURL = filePath(new URL(siteInfo.logo.Url));
	} catch (err) {
		console.log("Invalid DB custom icon URL");
	}
}

const menuitemsold = [
	{
		title: "Features",
		path: "#",
		children: [
			{ title: "Action", path: "/" },
			{ title: "Another action", path: "#" },
			{ title: "Dropdown Submenu", path: "#" },
			{ title: "404 Page", path: "/404" },
		],
	},
	{
		title: "Pricing",
		path: "/pricing",
	},
	{
		title: "About",
		path: "/about",
	},
	{
		title: "Blog",
		path: "/blog",
	},
	{
		title: "Contact",
		path: "/contact",
	},
	{
		title: "Pro Version",
		badge: true,
		path: "https://astroship-pro.web3templates.com/",
	},
];
---

<div class="max-w-screen-xl px-5">
	<StickyHeader
		class="sticky top-0 z-20 transition-all"
		scrollY={50}
		defaultClass="py-5 border-transparent"
		activeClass="py-2 backdrop-blur-lg"
	>
		<Astronav closeOnClick>
			<div class="flex w-full items-center justify-between lg:w-auto">
				<a
					href="/"
					class="inline-flex items-center grayscale hover:filter-none sm:relative sm:inline-block"
					aria-current={url.pathname === "/" ? "page" : false}
				>
					{
						siteInfo.logo && siteInfo.logo.Type === "emoji" ? (
							<>
								<span class="sm:h-15 sm:w-15 me-3 h-10 w-10 sm:absolute sm:start-[-4.5rem] sm:me-0">
									{siteInfo.logo.Emoji}
								</span>
								<span class="text-xl font-bold sm:text-2xl">{siteInfo.title}</span>
							</>
						) : siteInfo.logo && siteInfo.logo.Type === "external" ? (
							<>
								<img
									class="sm:h-15 sm:w-15 me-3 h-10 w-10 object-contain sm:absolute sm:start-[-4.5rem] sm:me-0"
									src={siteInfo.logo.Url}
								/>
								<span class="text-xl font-bold sm:text-2xl">{siteInfo.title}</span>
							</>
						) : siteInfo.logo && siteInfo.logo.Type === "file" && customIconURL ? (
							<>
								<img
									src={customIconURL}
									class="sm:h-15 sm:w-15 me-3 h-10 w-10 object-contain sm:start-[-4.5rem] sm:me-0"
								/>
								<span class="text-xl font-bold sm:text-2xl">{siteInfo.title}</span>
							</>
						) : (
							<span class="text-xl font-bold sm:text-2xl">{siteInfo.title}</span>
						)
					}
				</a>
				<div class="block lg:hidden">
					<MenuIcon class="h-4 w-4 text-gray-800" />
				</div>
			</div>
			<MenuItems class="mt-2 hidden w-full lg:mt-0 lg:flex lg:w-auto">
				<ul class="flex flex-col lg:flex-row lg:gap-3">
					{
						menuitems.map((item, index) => (
							<>
								{item.children && (
									<Dropdown
										title={item.title}
										children={item.children}
										lastItem={index === menuitems.length - 1}
									/>
								)}

								{!item.children && (
									<li>
										<a
											href={item.path}
											class="flex items-center py-2 text-gray-600 hover:text-gray-900 lg:px-3"
										>
											<span> {item.title}</span>
											{item.badge && (
												<span class="ml-1 animate-pulse rounded-full bg-indigo-600 px-2 py-0.5 text-[10px] font-semibold uppercase text-white">
													New
												</span>
											)}
										</a>
									</li>
								)}
							</>
						))
					}
				</ul>
				<div class="mt-3 flex items-center gap-4 lg:hidden">
					<Link href="#" style="muted" block size="md">Log in</Link>
					<Link href="#" size="md" block>Sign up</Link>
				</div>
			</MenuItems>
		</Astronav>
		<div>
			<div class="hidden items-center gap-4 lg:flex">
				<a href="#">Log in</a>
				<Link href="#" size="md">Sign up</Link>
			</div>
		</div>
	</StickyHeader>
</div>
I don’t know what I messed up, restart time for CSS, dumping modified header code here (Changed <main id="main" class="mx-auto max-w-prose px-8"> to have no class
---
import ThemeToggle from "../ThemeToggle.astro";
import Search from "../Search.astro";
import { getMenu } from "@/utils";
import { siteInfo } from "@/site.config";
import { filePath } from "@/lib/blog-helpers";

const menuLinks = await getMenu();

const url = new URL(Astro.request.url);

let customIconURL: string = "";
if (siteInfo.logo && siteInfo.logo.Type === "file") {
	try {
		customIconURL = filePath(new URL(siteInfo.logo.Url));
	} catch (err) {
		console.log("Invalid DB custom icon URL");
	}
}
---

<script>
	class MobileNavBtn extends HTMLElement {
		constructor() {
			super();

			const mobileButtonEl = document.getElementById("toggle-navigation-menu") as HTMLButtonElement;
			let menuOpen = false;

			function toggleMobileMenu() {
				const headerEl = document.getElementById("main-header")!;
				headerEl.classList.toggle("menu-open");
				menuOpen = !menuOpen;
				mobileButtonEl.setAttribute("aria-expanded", menuOpen.toString());
			}

			mobileButtonEl.addEventListener("click", () => toggleMobileMenu());

			// document.addEventListener("astro:after-swap", () => {
			// 	if (menuOpen) toggleMobileMenu();
			// });
		}
	}

	customElements.define("mobile-button", MobileNavBtn);

	document.addEventListener("DOMContentLoaded", function () {
		var toggleButton = document.getElementById("toggle-navigation-menu");

		document.addEventListener("click", function (event) {
			var navMenu = document.getElementById("navigation-menu");
			var mainHeader = document.getElementById("main-header");

			// Check if the click is on the toggle button or a descendant of it
			var isClickInsideToggleButton = toggleButton.contains(event.target);

			if (!isClickInsideToggleButton && event.target instanceof Node) {
				var isClickInsideNav = navMenu.contains(event.target);

				if (!isClickInsideNav && mainHeader) {
					// If the click is outside the navMenu and mainHeader exists, remove 'menu-open' attribute
					mainHeader.classList.remove("menu-open");
				}
			}
		});
	});
</script>
<!-- <div class="mx-auto max-w-screen-xl px-5"> -->
<header
	id="main-header"
	class="group relative my-5 mb-16 flex items-center justify-between sm:ps-[4.5rem]"
	transition:animate="none"
>
	<div class="flex w-full items-center justify-between lg:w-auto">
		<a
			href="/"
			class="inline-flex items-center grayscale hover:filter-none sm:relative sm:inline-block"
			aria-current={url.pathname === "/" ? "page" : false}
		>
			{
				siteInfo.logo && siteInfo.logo.Type === "emoji" ? (
					<>
						<span class="sm:h-15 sm:w-15 me-3 h-10 w-10 sm:absolute sm:start-[-4.5rem] sm:me-0">
							{siteInfo.logo.Emoji}
						</span>
						<span class="text-xl font-bold sm:text-2xl">{siteInfo.title}</span>
					</>
				) : siteInfo.logo && siteInfo.logo.Type === "external" ? (
					<>
						<img
							class="sm:h-15 sm:w-15 me-3 h-10 w-10 object-contain sm:absolute sm:start-[-4.5rem] sm:me-0"
							src={siteInfo.logo.Url}
						/>
						<span class="text-xl font-bold sm:text-2xl">{siteInfo.title}</span>
					</>
				) : siteInfo.logo && siteInfo.logo.Type === "file" && customIconURL ? (
					<>
						<img
							src={customIconURL}
							class="sm:h-15 sm:w-15 me-3 h-10 w-10 object-contain sm:absolute sm:start-[-4.5rem] sm:me-0"
						/>
						<span class="text-xl font-bold sm:text-2xl">{siteInfo.title}</span>
					</>
				) : (
					<span class="text-xl font-bold sm:text-2xl">{siteInfo.title}</span>
				)
			}
		</a>
	</div>

	<nav
		id="navigation-menu"
		class="absolute -inset-x-4 top-14 mt-2 flex hidden w-full flex-col items-end rounded-md bg-bgColor/[.97] py-2 text-accent shadow backdrop-blur group-[.menu-open]:z-50 group-[.menu-open]:flex sm:static sm:z-auto sm:-ms-4 sm:mt-1 sm:flex sm:flex-row sm:items-center sm:rounded-none sm:bg-transparent sm:py-0 sm:shadow-none sm:backdrop-blur-none lg:mt-0 lg:flex lg:w-auto lg:flex-row lg:gap-3"
		aria-label="Main menu"
	>
		{
			menuLinks.map((link) => (
				<a
					href={link.path}
					class="flex flex-col px-4 py-4 sm:py-0 sm:hover:underline lg:flex-row lg:gap-3"
					aria-current={url.pathname === link.path ? "page" : false}
					rel="prefetch"
				>
					{link.title}
				</a>
			))
		}
	</nav>
	<div class="items-center gap-4 lg:flex">
		<Search />
		<ThemeToggle />
	</div>
	<mobile-button>
		<button
			id="toggle-navigation-menu"
			class="group relative ms-4 h-7 w-7 sm:invisible sm:hidden"
			type="button"
			aria-label="Open main menu"
			aria-expanded="false"
			aria-haspopup="menu"
		>
			<svg
				id="line-svg"
				class="absolute start-1/2 top-1/2 h-full w-full -translate-x-1/2 -translate-y-1/2 transition-all group-aria-expanded:scale-0 group-aria-expanded:opacity-0"
				aria-hidden="true"
				focusable="false"
				xmlns="http://www.w3.org/2000/svg"
				fill="none"
				viewBox="0 0 24 24"
				stroke-width="1.5"
				stroke="currentColor"
			>
				<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 9h16.5m-16.5 6.75h16.5"
				></path>
			</svg>
			<svg
				id="cross-svg"
				class="absolute start-1/2 top-1/2 h-full w-full -translate-x-1/2 -translate-y-1/2 scale-0 text-accent opacity-0 transition-all group-aria-expanded:scale-100 group-aria-expanded:opacity-100"
				class="text-accent"
				aria-hidden="true"
				focusable="false"
				xmlns="http://www.w3.org/2000/svg"
				fill="none"
				viewBox="0 0 24 24"
				stroke-width="1.5"
				stroke="currentColor"
			>
				<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"></path>
			</svg>
		</button>
	</mobile-button>
</header>
<!-- </div> -->

I THINK IT IS THE CODEPEN EMBED CAUSING ISSUES ON LARGE SCREENS WITH ToC! NOT THE CODE 😠

I do not know what it is

Discarding some commits

import { filePath } from "../../../lib/blog-helpers";
import EmbedGeneric from "./EmbedGeneric.astro";
export interface Props {
	block: interfaces.Block;
@@ -32,7 +33,9 @@ if (block.Video.External) {
						allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
						allowfullscreen
					/>
				) : null
				) : (
					<EmbedGeneric url={url} />
				)
			) : (
style="max-width: 605px;"
	class="tiktok-embed"
	cite={url.toString()}
	data-video-id={videoId}
	style="max-width: 605px;"
	muted
>
<iframe
	allowtransparency="true"
	class="max-w-sm:h-[220px] h-[340px] w-full border-none"
	src={url.toString()}
	loading="lazy"></iframe>

in index.ts

export async function getMenu(): Promise<
  { title: string; path: string; children?: { title: string; path: string }[] }[]
> {

The fudge man, this is the issue? I had removed max-w-prose from here.

<PostLayout post={post} headings={headings}>
	<div class="post-body max-w-prose">
		<NotionBlocks blocks={blocks} isRoot={true} />
	</div>
</PostLayout>

Okay, so moving max-w-prose from body to main basically does not work. Great! Wrapping the main into a different div and applying max-w-prose to it also does not work.

Social icons in footer

Astro Icon also supports Iconify out-of-the-box with minimal configuration. To use an icon set from Iconify follow the instructions below:

  1. Find an Icon Set to use on the Iconify Icon Sets website
  2. Install the package (eg. npm i -D @iconify-json/mdi)
  3. Add an entry to the astro.config.mjs file astro.config.mjs

This does not work. Apparently all icons shown on icones are not in astro-icon 🤷‍♀️

Make footer look nicer
giscus Show information for the linked content
Process
giscus: 
    repo: nerdymomocat/nerdymomocat.github.io
    repo-id: R_kgDOJoxbeA
    category: Announcements
    category-id: DIC_kwDOJoxbeM4CWzrO
    mapping: pathname
    loading: lazy
    input-position: top
    reactions-enabled: true
toggle headings click issue
Done by adding a javascript that adds details to the closest parent
<details class="toggle mb-0 mt-8">
			<summary class="flex cursor-pointer list-none gap-2">
				<div>
					<svg
						class="rotate-svg mt-1 flex-shrink-0 transition-all duration-300"
						fill="none"
						height="20"
						width="20"
						stroke="currentColor"
						stroke-linecap="round"
						stroke-linejoin="round"
						stroke-width="2"
						viewBox="0 0 24 24"
					>
						<polyline points="12 8 18 14 12 20" />
					</svg>
				</div>
				<a
					class="inline"
					href={`#${id}`}
					id={id}
					onclick="var fullUrl = `${window.location.origin}${window.location.pathname}#${this.id}`; navigator.clipboard.writeText(fullUrl); this.closest('details').open = !this.closest('details').open;"
				>
					<h4 class="toggle-h4 mb-4 mt-8 inline text-lg">
						{block.Heading3.RichTexts.map((richText: interfaces.RichText) => (
							<RichText richText={richText} blockID={block.Id} />
						))}
					</h4>
				</a>
			</summary>
			<div class="pl-10">
				{block.Heading3.Children && (
					<NotionBlocks blocks={block.Heading3.Children} headings={headings} />
				)}
			</div>
		</details>
And the mobile menu is transparent again?
Nov 27, 2023, 09:35 PM
Github workflow
Suggested github pages astro file
# Sample workflow for building and deploying an Astro site to GitHub Pages
#
# To get started with Astro see: https://docs.astro.build/en/getting-started/
#
name: Deploy Astro site to Pages

on:
  # Runs on pushes targeting the default branch
  push:
    branches: ["main"]

  # Allows you to run this workflow manually from the Actions tab
  workflow_dispatch:

# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
  contents: read
  pages: write
  id-token: write

# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
concurrency:
  group: "pages"
  cancel-in-progress: false

env:
  BUILD_PATH: "." # default value when not using subfolders
  # BUILD_PATH: subfolder

jobs:
  build:
    name: Build
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3
      - name: Detect package manager
        id: detect-package-manager
        run: |
          if [ -f "${{ github.workspace }}/yarn.lock" ]; then
            echo "manager=yarn" >> $GITHUB_OUTPUT
            echo "command=install" >> $GITHUB_OUTPUT
            echo "runner=yarn" >> $GITHUB_OUTPUT
            exit 0
          elif [ -f "${{ github.workspace }}/package.json" ]; then
            echo "manager=npm" >> $GITHUB_OUTPUT
            echo "command=ci" >> $GITHUB_OUTPUT
            echo "runner=npx --no-install" >> $GITHUB_OUTPUT
            exit 0
          else
            echo "Unable to determine package manager"
            exit 1
          fi
      - name: Setup Node
        uses: actions/setup-node@v3
        with:
          node-version: "16"
          cache: ${{ steps.detect-package-manager.outputs.manager }}
          cache-dependency-path: ${{ env.BUILD_PATH }}/package-lock.json
      - name: Setup Pages
        id: pages
        uses: actions/configure-pages@v3
      - name: Install dependencies
        run: ${{ steps.detect-package-manager.outputs.manager }} ${{ steps.detect-package-manager.outputs.command }}
        working-directory: ${{ env.BUILD_PATH }}
      - name: Build with Astro
        run: |
          ${{ steps.detect-package-manager.outputs.runner }} astro build \
            --site "${{ steps.pages.outputs.origin }}" \
            --base "${{ steps.pages.outputs.base_path }}"
        working-directory: ${{ env.BUILD_PATH }}
      - name: Upload artifact
        uses: actions/upload-pages-artifact@v2
        with:
          path: ${{ env.BUILD_PATH }}/dist

  deploy:
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    needs: build
    runs-on: ubuntu-latest
    name: Deploy
    steps:
      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v2
Current file copied from my previous attempts
name: Deploy to GitHub Pages

on:
  # push:
  #   branches: [ main ]
  # schedule:
  #   - cron: '0 */8 * * *'
  workflow_dispatch:

permissions:
  contents: read
  pages: write
  id-token: write

env:
  NOTION_API_SECRET: ${{ secrets.NOTION_API_SECRET }}

jobs:
  build:
    runs-on: ubuntu-latest
    environment: github-pages
    steps:
      - name: Checkout your repository using git
        uses: actions/checkout@v3

      - name: Set and log environment secrets and variables
        run: |
          # echo "DATABASE_ID = $DATABASE_ID"
          echo "NOTION_API_SECRET = $NOTION_API_SECRET"

      - name: Install, build, and upload your site
        uses: withastro/action@v1
        with:
          path: .
          node-version: 20

  deploy:
    needs: build
    runs-on: ubuntu-latest
    environment:
      name: github-pages
    steps:
      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v1
Modification attempt
name: Deploy to GitHub Pages

on:
  # push:
  #   branches: [ main ]
  # schedule:
  #   - cron: '0 */8 * * *'
  workflow_dispatch:

permissions:
  contents: read
  pages: write
  id-token: write

concurrency:
  group: "pages"
  cancel-in-progress: true

env:
  BUILD_PATH: "."
  NOTION_API_SECRET: ${{ secrets.NOTION_API_SECRET }}

jobs:
  build:
    runs-on: ubuntu-latest
    environment: github-pages
    steps:
      - name: Checkout your repository using git
        uses: actions/checkout@v3

      - name: Setup Node
        uses: actions/setup-node@v3
        with:
          node-version: 21
          cache: 'npm'
          cache-dependency-path: ${{ env.BUILD_PATH }}/package-lock.json

      - name: Setup Pages
        id: pages
        uses: actions/configure-pages@v3

      - name: Install dependencies
        run: npm ci
        working-directory: ${{ env.BUILD_PATH }}

      - name: Set and log environment secrets and variables
        run: |
          # echo "DATABASE_ID = $DATABASE_ID"
          echo "NOTION_API_SECRET = $NOTION_API_SECRET"
          echo "GITHUB_PAGES = true"
          echo "SITE = ${{ steps.pages.outputs.origin }}"
          echo "BASE = ${{ steps.pages.outputs.base_path }}"

      - name: Build with Astro
        env:
          NOTION_API_SECRET: ${{ secrets.NOTION_API_SECRET }}
          GITHUB_PAGES: true
          SITE: ${{ steps.pages.outputs.origin }}
          BASE: ${{ steps.pages.outputs.base_path }}
        run: |
          echo "NOTION_API_SECRET = $NOTION_API_SECRET"
          echo "GITHUB_PAGES = $GITHUB_PAGES"
          echo "SITE = $SITE"
          echo "BASE = $BASE"
          npx --no-install astro build
          # or if you prefer yarn, use: yarn build
          # --site and --base flags can be included if needed for Astro
          # --site steps.pages.outputs.origin" \
          # --base steps.pages.outputs.base_path"
        working-directory: ${{ env.BUILD_PATH }}

      - name: Upload artifact
        uses: actions/upload-pages-artifact@v2
        with:
          path: ${{ env.BUILD_PATH }}/dist



  deploy:
    needs: build
    runs-on: ubuntu-latest
    name: Deploy
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    steps:
      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v2


Alright, as expected site/base is messed up because it is hardcoded but I should have that fixed by tonight and we are done!
//Might need to modify this to also include collection paths
export const getPageLink = (page: number, tag: string) => {
  if (page === 1) {
    return tag ? getTagLink(tag) : path.join(BASE_PATH, "/");
  }
  return tag
    ? path.join(BASE_PATH, `/posts/tag/${encodeURIComponent(tag)}/page/${page.toString()}`)
    : path.join(BASE_PATH, `/posts/page/${page.toString()}`);
};
Pagefind does not work?
Some final bug squashes Nov 27, 2023, 07:00 PM
💡
Well, you cannot fork your own repo and I want to use that repo as a template, so irritating! Now, searching for a solution.
Now transferring giscs is an issue and someone has had that problem before of course

Specifically here

Weird two line menu items on half screen, maybe set the font size to text-sm on mid sized screens?
I thought I wanted rank of pages to change but I don’t
Nov 28, 2023, 11:15 PM
TF is up with performance?
Screenshots

ALSO GOOGLE

Performance

Keywords: #On-Demand Incremental Static Regeneration

Basic steps:
use two files buildtimestart.txt and buildtimeend.txt in the folder .tmp/
know that the last updated time does not pass to parent blocks other than pages; so if a post is updated, you cannot use some blocks and not use some blocks; do not make it complicated
remember notion does not tell you when pages have been deleted, so they will still exist in the cache? OMG? maybe I do retrieve every post in a call but use cached blocks — wtf.
Points

Steps:

Add partytown for GA script
Check how many times getAllPosts is being called
Cache getAllPosts results and move filters out
Cache block results but invalidate them if post is new
Cache og-images generated or just generate them in public folder so that they are easier to figure out if they have been cached?
Cache things downloaded from Notion
Process
Nov 29, 2023, 05:48 AM
change how posts are filtered
change getAllPosts
change getAllPosts references
change getAllPostsAndPages references

Complicated cache:

From otoyo
"scripts": {
    "dev": "astro dev",
    "start": "astro dev",
    "build": "astro build",
    "build:cached": "npm run cache:fetch && astro build",
    "preview": "astro preview",
    "astro": "astro",
    "lint": "eslint --ext .js,.ts,.astro src",
    "format": "npx prettier --write .",
    "cache:fetch": "node scripts/blog-contents-cache.cjs",
    "cache:purge": "nx reset && rm -f tmp/*",
    "_fetch-notion-blocks": "node scripts/retrieve-block-children.cjs"
  },
Articles
Dec 2, 2023, 08:49 AM

I tried using <Code> component from Astro, but it is hard to add styles to it

For lightbox, doing import "../styles/glightbox.css" adds some error to captions

I need to use import

I need to use window loaded or something to connect it

Can’t get dynamic jsx based import to work

Landed on CDN for both

And need npm install glightbox

Dec 3, 2023, 07:51 AM
Mainly adding stuff for on this page and linked to this page
get all on this page
get all to this page
decide on a tooltip library
trying flowbite
Dec 3, 2023, 11:18 PM
I got this error once while building but cannot reproduce it: error Cannot read properties of undefined (reading 'title')
Orphan heading found: Heading 2.
@notionhq/client warn: request fail {
  code: 'object_not_found',
  message: 'Could not find block with ID: 3cb777e3-4815-41c8-8f30-d790b6ce7685. Make sure the relevant pages and databases are shared with your integration.'
}
Request was aborted
look at tailwind aspect ratio plugin that is imported later (might help with google maps and everything there!!
popover
fix current page block popover for links in post body
add bookmark as popover for external links?
add references to table of contents
FLOWBITE IS SO DARN HEAVY

The general performance is not perfect anyway

Trying preline
Nah, pretty much the same: first load of preline || Mobile
Nah, pretty much the same: first load of preline || Mobile
Second load on preline || mobile
Second load on preline || mobile
Second load on flowbite || Mobile
Second load on flowbite || Mobile
Nah, pretty much the same: first load of preline || Desktop
Nah, pretty much the same: first load of preline || Desktop
Second load on flowbite || Desktop
Second load on flowbite || Desktop
Second load on flowbite || Desktop
Second load on flowbite || Desktop


Npage
<span class="text-link no-underline hover:underline hover:decoration-accent-2 hover:underline-offset-4"><button data-popover-target=`popover-description-${id}` data-popover-placement="bottom-end" type="button" id=`popover-click-${id}`>{popoverSpanText?popoverSpanText:<slot/>}<span class="sr-only">Show information for the linked content</span></button></span>
  <div data-popover id=`popover-description-${id}` role="tooltip" class="absolute z-10 invisible inline-block text-sm text-gray-500 transition-opacity duration-300 bg-white border border-gray-200 rounded-lg shadow-sm opacity-0 w-72 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-400">
    <a href={linkedTo}>
      <div class="p-3 space-y-2">
          <h3 class="font-semibold underline decoration-wavy decoration-accent-2 text-accent">{popoverTitle}</h3>
          {excerpt && <p>{excerpt}</p>}
          <a href={linkedTo} class="flex items-center font-medium text-blue-600 dark:text-blue-500 dark:hover:text-blue-600 hover:text-blue-700 hover:underline">{linkText}<svg class="w-2 h-2 ms-1.5 rtl:rotate-180" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 6 10">
      <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 9 4-4-4-4"/>
    </svg></a>
      </div>
    </a>
      <div data-popper-arrow></div>
  </div>
NBlock
<span class="text-link no-underline hover:underline hover:decoration-accent-2 hover:underline-offset-4"><button data-popover-target=`popover-description-${id}` data-popover-placement="bottom-end" type="button">{popoverSpanText?popoverSpanText:<slot/>}<span class="sr-only">Show information for the linked content</span></button></span>
  <div data-popover id=`popover-description-${id}` role="tooltip" class="absolute z-10 invisible inline-block text-sm text-gray-500 transition-opacity duration-300 bg-white border border-gray-200 rounded-lg shadow-sm opacity-0 w-72 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-400">
    <a href={linkedTo}>
      <div class="p-3 space-y-2">
          {popoverTitle && <h3 class="font-semibold font-semibold underline decoration-wavy decoration-accent-2 text-accent">{popoverTitle}</h3>}
          <NotionBlocks blocks={[block]} renderChildren={false}/>
          <a href={linkedTo} class="flex items-center font-medium text-blue-600 dark:text-blue-500 dark:hover:text-blue-600 hover:text-blue-700 hover:underline">{linkText}<svg class="w-2 h-2 ms-1.5 rtl:rotate-180" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 6 10">
      <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 9 4-4-4-4"/>
    </svg></a>
      </div>
    </a>
      <div data-popper-arrow></div>
  </div>
This does not work
REFERENCES && (
<script>
  import {initPopovers} from 'flowbite';
  window.addEventListener('load', () => {
    initPopovers();
  });
</script>
  )
}

Neither does this

{
REFERENCES && (
<script is:inline defer>
  import {initPopovers} from 'flowbite';
  window.addEventListener('load', () => {
    initPopovers();
  });
</script>
  )
}
Maybe the new popover API? (too complicated to position and auto close!!)
<span class="text-link no-underline hover:underline hover:decoration-accent-2 hover:underline-offset-4"><button popovertarget=`popover-description-${id}` type="button" id=`popover-click-${id}`>{popoverSpanText?popoverSpanText:<slot/>}<span class="sr-only">Show information for the linked content</span></button></span>
  <div popover id=`popover-description-${id}` role="tooltip">
    <a href={linkedTo}>
      <div class="p-3 space-y-2">
          <h3 class="font-semibold underline decoration-wavy decoration-accent-2 text-accent">{popoverTitle}</h3>
          {excerpt && <p>{excerpt}</p>}
          <a href={linkedTo} class="flex items-center font-medium text-blue-600 dark:text-blue-500 dark:hover:text-blue-600 hover:text-blue-700 hover:underline">{linkText}<svg class="w-2 h-2 ms-1.5 rtl:rotate-180" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 6 10">
      <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 9 4-4-4-4"/>
    </svg></a>
      </div>
    </a>
      <div data-popper-arrow></div>
  </div>
Dec 8, 2023, 02:20 AM
The async tag on mermaid was making it not render at all. Oh, and the import mermaid from mermaid works there 🤷‍♀️
Fixing scroll on bottom nav and wth is this code with double ! ?
function callback(entries: IntersectionObserverEntry[]) {
      entries.forEach((entry) => {
        if (entry.target === targetHeader) {
          scrollBtn.dataset.show = (!entry.isIntersecting).toString();
        }

        const isBottomNavVisible = !entries.some((entry) => entry.isIntersecting);
        bottomTocNav.classList.toggle("hidden", !isBottomNavVisible);
      });
    }
btw, the audio element in popup is causing horizontal scroll:
lol at z-index and gpt absolutely making sure to be incorrect (patchy fix - does not work for gray/light-gray)
GPT
Dec 8, 2023

Updating to astro 4

Dec 9, 2023, 11:53 PM
Astro-tweet has not updated and I do not like looks of @astro-community/astro-embed-twitter . Maybe I just copy paste the component from astro-tweet
Remove URL normalization for same page popups on anchor links (the interlinked content part works fine)
Add URL normalization for different page links in popups
No need. Somehow on build, there is a trailing slash added, but not in dev? So, basically if I do navlink#block-id, that causes issues in build but not in dev — if I do navlink/#block-id that causes issues in dev but not in build.
How it looks in dev (no trailing slash)
How it looks in dev (no trailing slash)
Has trailing slash in build?
Has trailing slash in build?
Dec 10, 2023, 09:20 PM
Popover to floating ui

So, I have the basic code and I have tried a million methods but they don’t really work to have a clean outcome.

This is the very basic code that I understand right now
<script type="module">
  import { computePosition, offset, shift, flip, autoUpdate } from 'https://cdn.jsdelivr.net/npm/@floating-ui/dom@1.5.3/+esm';

  document.addEventListener('DOMContentLoaded', () => {
  const popoverTriggers = document.querySelectorAll('[data-popover-target]');
  let openPopovers = [];
  let cleanupAutoUpdate = new Map();
  let hoverTimeout;

  const hideAllPopovers = () => {
    openPopovers.forEach(popoverEl => hidePopover(popoverEl));
    };

  const hidePopover = (popoverEl) => {
    if (popoverEl) {
      popoverEl.style.visibility = 'hidden';
      popoverEl.style.opacity = '0';
      popoverEl.style.top = '0px';
      popoverEl.style.left = '0px';

      const cleanup = cleanupAutoUpdate.get(popoverEl);
      if (cleanup) {
        cleanup();
        cleanupAutoUpdate.delete(popoverEl);
      }
      const openPopoverIndex = openPopovers.indexOf(popoverEl);
      if (openPopoverIndex !== -1) {
        openPopovers.splice(openPopoverIndex, 1);
      }
    }
  };


  const showPopover = (triggerEl, popoverEl) => {

    const update = () => {
      computePosition(triggerEl, popoverEl, {
        middleware: [shift({ padding: 5 }), flip({ padding: 5 }), offset(10)]
      }).then(({ x, y }) => {
        Object.assign(popoverEl.style, {
          left: `${x}px`,
          top: `${y}px`,
          position: 'absolute'
        });
      });
    };

    update();
    popoverEl.style.visibility = 'visible';
    popoverEl.style.opacity = '1';
    openPopovers.push(popoverEl);
    cleanupAutoUpdate.set(popoverEl, autoUpdate(triggerEl, popoverEl, update));
  };

  popoverTriggers.forEach(triggerEl => {
    const popoverID = triggerEl.dataset.popoverTarget;
    const popoverEl = document.getElementById(popoverID);

    triggerEl.addEventListener('mouseenter', () => {
      clearTimeout(hoverTimeout);
      showPopover(triggerEl, popoverEl);
    });

    triggerEl.addEventListener('mouseleave', () => {
      hoverTimeout = setTimeout(() => {
          hidePopover(popoverEl);
      }, 100);
    });

    triggerEl.addEventListener('focus', () => {
      showPopover(triggerEl, popoverEl);
    });

    triggerEl.addEventListener('blur', () => {
      hidePopover(popoverEl);
    });

    popoverEl.addEventListener('mouseenter', () => {
      clearTimeout(hoverTimeout);
    });

    popoverEl.addEventListener('mouseleave', () => {
      hidePopover(popoverEl);
    });
  });

  document.addEventListener('click', (event) => {
    if (!event.target.closest('[data-popover-target]')) {
      hideAllPopovers();
    }
  });

  document.addEventListener('keydown', (event) => {
    if (event.key === 'Escape') {
      hideAllPopovers();
    }
  });
});

</script>

The problem is, if I move from one one triggerEl to another triggerEl quickly within 100ms, then nothing closes that has already been opened. Why? Because it goes to mouseleave here which has a timeout of 100ms to be able to move cursor to popover offset.

    triggerEl.addEventListener('mouseenter', () => {
      clearTimeout(hoverTimeout);
      showPopover(triggerEl, popoverEl);
    });

    triggerEl.addEventListener('mouseleave', () => {
      hoverTimeout = setTimeout(() => {
          hidePopover(popoverEl);
      }, 100);
    });

It should call this and at least close after 100ms BUT the hovertimeout is universal:

  const hidePopover = (popoverEl) => {
    if (popoverEl) {
      popoverEl.style.visibility = 'hidden';
      popoverEl.style.opacity = '0';
      popoverEl.style.top = '0px';
      popoverEl.style.left = '0px';

      const cleanup = cleanupAutoUpdate.get(popoverEl);
      if (cleanup) {
        cleanup();
        cleanupAutoUpdate.delete(popoverEl);
      }
      const openPopoverIndex = openPopovers.indexOf(popoverEl);
      if (openPopoverIndex !== -1) {
        openPopovers.splice(openPopoverIndex, 1);
      }
    }
  };

So, entering into a new triggerEl just clears the universal hovertimeout. And the hidePopover never gets called.

This is a good thing for nested triggers, because we want their parents to not be closed. But we want only 1 popup of each LEVEL. Let me explain level.

For example: Page has Trigger A and Trigger B. Trigger A has a popover which has Trigger A1 and Trigger A2. Trigger A2 has a popup that has Trigger A21 and Trigger A22.

Say, I hover over Trigger A and then suddenly move under 100ms to Trigger B, so popover from Trigger A will never close because moving to Trigger B has cleared timeout.

But if I hover over Trigger A, and it opens a popup, we hover over Trigger A1 inside the popover from trigger A, and that makes it so that the parent popup does not close which is a good thing. Because we want one popup each level. But if we also suddenly move from Trigger A1 to Trigger A2, the popup from Trigger A1 will not close too because moving to Trigger A2 clears timeout.

The complicated part here is maintaining and tracking levels. Especially consider that this code adds event handlers to all triggers no matter how nested, so this tracking needs to be taken care of in showPopover (maybe add trigger element and the containment level somewhere, check if there is a document contain or popover contain match for trigger element, and then immediately hidepopover if there is).

  popoverTriggers.forEach(triggerEl => {
    const popoverID = triggerEl.dataset.popoverTarget;
    const popoverEl = document.getElementById(popoverID);

    triggerEl.addEventListener('mouseenter', () => {
      clearTimeout(hoverTimeout);
      showPopover(triggerEl, popoverEl);
    });

    triggerEl.addEventListener('mouseleave', () => {
      hoverTimeout = setTimeout(() => {
          hidePopover(popoverEl);
      }, 100);
    });

    triggerEl.addEventListener('focus', () => {
      showPopover(triggerEl, popoverEl);
    });

    triggerEl.addEventListener('blur', () => {
      hidePopover(popoverEl);
    });

    popoverEl.addEventListener('mouseenter', () => {
      clearTimeout(hoverTimeout);
    });

    popoverEl.addEventListener('mouseleave', () => {
      hidePopover(popoverEl);
    });
  });
Dec 12, 2023, 06:30 AM

Finished a bunch of optimizations; I am not even sure if most of them worked, but at least now I know what is going into it!

Lol, should have known you can run lighthouse locally and not push stuff 100 times 🤣

^ Oh doesn’t really do what I want

Partytown in define vars thing

and without it

I will later try to figure out what happens with no partytown

Btw,

The main benefit of using lazysizes for me is just that it does not add lazy to the first image on the page, so it reduces that blocking time. If there is another way to do that, we should do it.

Dec 12, 2023, 07:45 PM

Hmm, even after changing popovers to hidden, it still takes really long

Lol

Note: In Firefox, the loading attribute must be defined before the src attribute, otherwise it has no effect (Firefox bug 1647077).

No, script based change to eager does not really work. As expected to be fair.
OMG WHAT THE HELL

i am gonna laugh so much if this works. i keep forgetting i can use ts files as normal js files for key store values and keep trying to pass build time variables through components or whatever that thing is. 🤣

Update  2023-12-12T12:53:00.000-08:00  I am laughing 😅 
Update Dec 12, 2023, 08:53 PM I am laughing 😅 

Dec 13, 2023, 06:05 AM Moved code highlighting to inbuilt shiki

Dec 13, 2023, 08:44 AM

Astro code highlighting


Jan 31, 2024, 09:54 PM
Apr 10, 2024, 08:49 AM

Trying to add collections by streaming a public page component into an iframe similar to embednotion.

This is where direct copy pasting gets me.
This is where direct copy pasting gets me.
Jun 25, 2024, 05:55 AM

It doesn’t have this:

The issue with transformers

I tried using both NACode and direct code component. It is like the second time this is called — transformerNotationFocus, it is not rendering?

      <NACode code={`const foo = 'hello'
const bar = ' world'
console.log(foo + bar)
`} lang={language}   transformers={[transformerNotationFocus()]}/>

I even tried changing styles to outside


  pre.has-focused .line:not(.focused) {
    @apply blur-[1px];
  }

The problem is repeated component calls

      <Code code={`const foo = 'hello'
const bar = ' world'
console.log(foo + bar)
`} lang={language}   transformers={[transformerNotationFocus()]}/>
<Code code={`const foo = 'hello'
const bar = ' world'
console.log(foo + bar)
`} lang={language}   transformers={[transformerNotationFocus()]}/>
Jun 25, 2024

Trying to make clipboard code better with these rules:

/*
Rules:
1. Two things to do: remove lines and clean up comments in remaining lines
2. For remove lines, only consider these commands: [!code --] or [!code warning] or [!code error]
3. Remove the line when the command is after the comment mark (any number of spaces between comment mark and coommand are fine). there can be text after command.
4. do not consider command or comment mark if they are part of a string literal enclosed with ` ` or " "
5. if a line is not removed, then consider these commands: [!code --] [!code ++] [!code warning] [!code error] [!code highlight] [!code highlight:number_here] [!code word:word_here:number_here] [!code word:word_here] [!code focus] [!code focus:number_here].
6. if these occur right after the comment mark (any number of spaces between comment mark and coommand are fine)
and are not part of string literal, just remove the command from if it occurs after the comment mark (and is not in string literal).
do not remove the line. there can be stuff after the command.
7. if the part after comment mark is empty or only white spaces, remeove the comment mark. again remember comment marks that are part of string literals are not considered for this.
*/

Nothing works and i have been trying for 3 hours now.

code at Jun 25, 2024, 06:37 PM

type CommentStyle = string | string[];

const commentStyles: Record<string, CommentStyle> = {
	// Double slash comments
	"//": [
		"default",
		"c",
		"cpp",
		"java",
		"javascript",
		"typescript",
		"csharp",
		"go",
		"swift",
		"kotlin",
		"scala",
		"rust",
		"dart",
		"css",
		"php",
		"pascal",
		"groovy",
	],

	// Hash comments
	"#": [
		"python",
		"ruby",
		"perl",
		"bash",
		"powershell",
		"php",
		"yaml",
		"toml",
		"r",
		"julia",
		"elixir",
	],

	// Double dash comments
	"--": ["haskell", "lua", "sql", "plsql", "ada"],

	// Percent comments
	"%": ["matlab", "octave", "latex", "katex", "erlang"],

	// Semicolon comments
	";": ["lisp", "clojure", "scheme", "ini"],

	// Other specific comment styles
	"⍝": ["apl"],
	"!": ["fortran"],
	"'": ["vb"],
	"*>": ["cobol"],
	"<!--": ["html"],

	// Languages without standard comment syntax
	"": ["brainfuck", "whitespace"],
};

// Invert the commentStyles object for easier lookup
const languageCommentStyles: Record<string, CommentStyle> = Object.entries(commentStyles).reduce(
	(acc, [style, langs]) => {
		(Array.isArray(langs) ? langs : [langs]).forEach((lang) => {
			acc[lang] = style;
		});
		return acc;
	},
	{} as Record<string, CommentStyle>,
);

const getCommentStyle = (language: string): CommentStyle => {
	return languageCommentStyles[language.toLowerCase()] || languageCommentStyles.default;
};

const escapeRegExp = (string: string): string => string.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&");

const createCommentRegex = (style: string | string[]): RegExp => {
  const patterns = Array.isArray(style) ? style : [style];
  const escapedPatterns = patterns.map(escapeRegExp).join("|");
  return new RegExp(`^(.*?)(${escapedPatterns})(.*)$`);
};

const notationRegex = /\[!code (focus|--|\+\+|highlight|word:[^\]]+|error|warning)\]/g;

const createRemoveLineRegex = (style: string | string[]): RegExp => {
  const patterns = Array.isArray(style) ? style : [style];
  const escapedPatterns = patterns.map(escapeRegExp).join("|");
  return new RegExp(`(${escapedPatterns})\\s*\\[!code (--|error|warning)\\](?!\\s{2,})`);
};

const processCode = (code: string, language: string): string => {
  if (!languageCommentStyles[language] || language === "plaintext") {
    return code;
  }

  const style = getCommentStyle(language);
  const commentRegex = createCommentRegex(style);
  const removeLineRegex = /^\s*(\/\/|#|--|%|;||!|'|\*>|<!--)\s*\[!code\s+(--|error|warning)\]/;
  const cleanupRegex = /\[!code\s+(--|error|warning|highlight|word:[^\]]+|\+\+|focus)(:[\d]+)?\]/g;

  return code
    .split("\n")
    .filter((line) => {
      const commentMatch = line.match(commentRegex);
      if (commentMatch) {
        const [, before, commentStart, commentContent] = commentMatch;
        // Check if the comment start is within a string literal
        const stringLiteralRegex = /(['"`])(?:(?!\1)[^\\]|\\.)*\1/g;
        let isInStringLiteral = false;
        let match;
        while ((match = stringLiteralRegex.exec(before)) !== null) {
          if (match[0].includes(commentStart)) {
            isInStringLiteral = true;
            break;
          }
        }
        if (isInStringLiteral) {
          return true;
        }
        // Check if the line should be removed
        return !removeLineRegex.test(line);
      }
      return true;
    })
    .map((line) => {
      return line.replace(commentRegex, (_, before, commentStart, commentContent) => {
        // Clean up the comment content
        const cleaned = commentContent.replace(cleanupRegex, (match) => {
          // Only remove the command if it's at the start of the comment
          return commentContent.trim().startsWith(match) ? "" : match;
        }).trim();
        if (cleaned) {
          return `${before}${commentStart} ${cleaned}`;
        } else {
          return before.trimEnd();
        }
      });
    })
    .join("\n");
};



// Usage remains the same
const code = block.Code.RichTexts.map(
	(richText: interfaces.RichText) => richText.Text.Content,
).join("");
const language =
	block.Code.Language.toLowerCase() === "plain text"
		? "plaintext"
		: block.Code.Language.toLowerCase();
const modifiedCode = processCode(code, language);

const code_inject = SHORTCODES["html"] ? code.startsWith(SHORTCODES["html"]) : false;
const plainTextCaption = block.Code?.Caption.map((richText) => richText.PlainText).join(" ");

While these rules make sense, shiki’s own regex isn’t this complicated:

Errors: in second line, comment mark is removed? and in last line it does remove comment mark from both places.

also btw, a line with this console.log('hewwo') console.log('hewwo') console.log('hewwo') console.log('hewwo') console.log('hewwo') console.log('hewwo') // [!code --] [!code focus] becaxztomes console.log('hewwo') console.log('hewwo') console.log('hewwo') console.log('hewwo') console.log('hewwo') console.log('hewwo') [!code focus]

and this is rendered as ++ console.log('use // [!code ++]') // [!code --] with text as: console.log('use')

So, it seems like they are just

console.log('hewwo') console.log('hewwo') console.log('hewwo') console.log('hewwo') console.log('hewwo') console.log('hewwo') 
console.log('hello') // [!code --] blah blah come!
console.log('goodbye') // hellllaaaaa [!code --] blah blah come!
console.log('goodbye') // [!code  --] blah blah come!
console.log('use // [!code --]') 
console.log('hewwo') console.log('hewwo') console.log('hewwo') console.log('hewwo') console.log('hewwo') console.log('hewwo') 
console.log('hello') # [!code --] blah blah come!
console.log('goodbye') # hellllaaaaa [!code --] blah blah come!
console.log('goodbye') # [!code  --] blah blah come!
console.log('# [!code --]') 
console.log('hewwo') console.log('hewwo') console.log('hewwo') console.log('hewwo') console.log('hewwo') console.log('hewwo') 
console.log('hello') % [!code --] blah blah come!
console.log('goodbye') % hellllaaaaa [!code --] blah blah come!
console.log('goodbye') % [!code  --] blah blah come!
console.log('\% [!code --]') 
<h1>Welcome to My Website</h1> 
<h1>Welcome to My Website</h1> <!--   [!code --] blah blah come! -->
<h1>Welcome to My Website</h1> <!--  helllaaa [!code --] -->
<h1>Welcome to My Website</h1> <!-- [!code  --] blah blah come! -->
<h1>Welcome  [!code ++] to My Website</h1> 

Okay, I think I'm done. Using expressive code is way more complicated because the markdown option does parse normally but the code option doesn't have just like internal parser. You can't just be like code equal to code. You have to parse every single thing out of it like deletions, insertions, etc. It feels like I'm writing a parser of my own and that would be a hassle. I went the easy route and like keyboard clipboard cleanup and did what Shaky was doing. It made more sense because if you wanted Shaky transformer highlighting then you would be using it properly anyway. So yeah.

Jun 26, 2024, 02:16 AM

Started a new block because this now about footnotes.

Footnote testing

[^abr3a]: footnote much before the mention itself

this is a normal footnote [^abra1]

[^abra1]: footnote content on a new block

this is a normal footnote marker with no space before[^abra2]or after
[^abra2]: footnote content on new line, same block

this is a common invalid footnote [^ abr3a]

this is a common valid footnote with no attached footnote content [^abra3]

double mention of same footnote marker [^abra2]

footnote marker for multiline footnote [^abra4]

[^abra4]: hey how are you?

multiline comment using indented blocks

what if it has a callout?

random info in footnote

footnote marker for multiline again? [^abra5]
[^abra5]: hey? how are you doing? multiline comment but in same block
what is up?

[^abra6]: what if i have footnote content that is not a footnote marked anywhere? markdown just removes this

This is what I have.

Here is what I am thinking I guess

  • Identify footnote content when it starts with [^anywordwithoutspace]:
    • The footnote content is consider till the end of that block including children. This will consider when footnote content is a separate block, when it is in same block at end, when it has multiline footnote with child blocks, when it has multiline footnotes using enter key.
    • The only place this does not work is when in a single block someone has multiline footnotes and then the footnote ends when they press double enter in same block. But I am ignoring that case, why are you using notion, end that block?
  • Identify footnote marker if any content anywhere in plaintext has [^anywordwithoutspace]. It is not needed that this marker have spaces around it.
Jun 26, 2024

Error in run here:

I tried making it so that avif is default and oh my god, it doubled the time and increased econnreset errors so much!

Jun 30, 2024

Tried making it so that tags aren’t always open on mobile but this is not currently working for pagefind filter

<script>
document.addEventListener('DOMContentLoaded', () => {
  if (window.innerWidth <= 640) {
    document.querySelectorAll('.pagefind-ui__filter-block').forEach(block => {
      block.removeAttribute('open');
      console.log("removed!");
    });
  }
});
</script>
Jul 2, 2024

If I dynamically generate popovers — it helps!

Before
Before
After
After
Jul 5, 2024

Figure out basic improvement on tables

Using simple datatables because i really do not want to import jquery too with datatables Jul 5, 2024, 05:47 PM

Options for simple datatables here.

https://fiduswriter.github.io/simple-datatables/demos/10-filters/

https://www.npmjs.com/package/simple-datatables/v/9.0.4?activeTab=code

https://fiduswriter.github.io/simple-datatables/documentation/

Jul 6, 2024

Removing the css does not fix these issues:

  • The top <thead> <tr> is white???
  • On search, the auto table layout does not work and overflow X is always none

It fixes this:

  • borders between cells are back

I just pushed it out 🤷‍♀️

Jul 7, 2024

Reducing div size. Both openai and claude suddenly recommended <template> tag. But know that in astro, that tag should be the end of the file or it is not processed correctly 🤷‍♀️.

SIGNIFICANT REDUCTION IN DOM SIZE

After (1200)
After (1200)
Before (2063)
Before (2063)

Merging Jul 7, 2024, 09:24 PM:

Before
Before
after
after

Figcaption issue

---
import { ENABLE_LIGHTBOX } from "../../../constants.ts";
import * as interfaces from "../../../lib/interfaces.ts";
import { filePath, getFirstImage } from "../../../lib/blog-helpers";
import Caption from "../Caption.astro";
// import GLightbox from 'glightbox';


export interface Props {
	block: interfaces.Block;
  setId?:boolean;
}

const { block, setId=true } = Astro.props;

let image = "";
if (block.NImage?.External) {
	image = block.NImage.External.Url;
} else if (block.NImage?.File) {
	image = filePath(new URL(block.NImage.File.OptimizedUrl));
}

const plainTextCaption = block.NImage?.Caption.map((richText) => richText.PlainText).join(' ');

---

<figure class="flex max-w-full mx-auto mt-1" id={setId?block.Id:undefined}>
  {
      image && (
          <div class="mx-auto min-w-0">
              <div>
                  {ENABLE_LIGHTBOX ? (
                      <a data-type="image" href={image} class="mediaglightbox" data-description={plainTextCaption} aria-label={`Open image with alt ${plainTextCaption} in lightbox`}>
                          <img class="imagemedia block max-w-full rounded-md"  loading={getFirstImage()?"eager":"lazy"} src={image} alt={plainTextCaption}/>
                      </a>
                  ) : (
                      <img class="imagemedia block max-w-full rounded-md" src={image} loading={getFirstImage()?"eager":"lazy"} alt={plainTextCaption}/>
                  )}
              </div>
              <Caption richTexts={block.NImage.Caption} block={block} as="figcaption" />
          </div>
      )
  }
</figure>
Before
---
import { ENABLE_LIGHTBOX } from "../../../constants.ts";
import * as interfaces from "../../../lib/interfaces.ts";
import { filePath, getFirstImage } from "../../../lib/blog-helpers";
import Caption from "../Caption.astro";
// import GLightbox from 'glightbox';


export interface Props {
	block: interfaces.Block;
  setId?:boolean;
}

const { block, setId=true } = Astro.props;

let image = "";
if (block.NImage?.External) {
	image = block.NImage.External.Url;
} else if (block.NImage?.File) {
	image = filePath(new URL(block.NImage.File.OptimizedUrl));
}

const plainTextCaption = block.NImage?.Caption.map((richText) => richText.PlainText).join(' ');

---

{image && (<figure class="max-w-full mx-auto mt-1" id={setId?block.Id:undefined}>
  {
              <div class="mx-auto min-w-0">
                  {ENABLE_LIGHTBOX ? (
                      <a data-type="image" href={image} class="mediaglightbox" data-description={plainTextCaption} aria-label={`Open image with alt ${plainTextCaption} in lightbox`}>
                          <img class="imagemedia block max-w-full rounded-md"  loading={getFirstImage()?"eager":"lazy"} src={image} alt={plainTextCaption}/>
                      </a>
                  ) : (
                      <img class="imagemedia block max-w-full rounded-md" src={image} loading={getFirstImage()?"eager":"lazy"} alt={plainTextCaption}/>
                  )}
              </div>
          <Caption richTexts={block.NImage.Caption} block={block} as="figcaption" />

  }
</figure>)}

Removing script=module thing. Cannot remove from glightbox btw 🤷‍♀️ or from floating-ui. But it works with removing from simple-datatables.

I tried adding lazyload to videos and it messed up the rating more???

Sep 13, 2024

Page link mentions now show up in API

I got this for example with a normal website
{
    "object": "block",
    "id": "4869b8e5-edb5-45b3-a3cf-c30674961562",
    "parent": {
        "type": "page_id",
        "page_id": "85cb68b6-b12f-4a62-af33-3348ea751f77"
    },
    "created_time": "2024-06-23T20:49:00.000Z",
    "last_edited_time": "2024-08-02T12:08:00.000Z",
    "created_by": {
        "object": "user",
        "id": "5aa7609b-54f8-42b7-b4a2-6f38a03279bd"
    },
    "last_edited_by": {
        "object": "user",
        "id": "5aa7609b-54f8-42b7-b4a2-6f38a03279bd"
    },
    "has_children": true,
    "archived": false,
    "in_trash": false,
    "type": "paragraph",
    "paragraph": {
        "rich_text": [
            {
                "type": "text",
                "text": {
                    "content": "Paste link as mention: ",
                    "link": null
                },
                "annotations": {
                    "bold": false,
                    "italic": false,
                    "strikethrough": false,
                    "underline": false,
                    "code": false,
                    "color": "default"
                },
                "plain_text": "Paste link as mention: ",
                "href": null
            },
            {
                "type": "mention",
                "mention": {
                    "type": "link_mention",
                    "link_mention": {
                        "href": "https://shiki.style/packages/transformers#shikijs-transformers",
                        "title": "Shiki",
                        "icon_url": "https://shiki.style/logo.svg",
                        "description": "A beautiful yet powerful syntax highlighter",
                        "link_author": "Pine Wu, Anthony Fu",
                        "thumbnail_url": "https://shiki.style/og.png"
                    }
                },
                "annotations": {
                    "bold": false,
                    "italic": false,
                    "strikethrough": false,
                    "underline": false,
                    "code": false,
                    "color": "default"
                },
                "plain_text": "https://shiki.style/packages/transformers#shikijs-transformers",
                "href": "https://shiki.style/packages/transformers#shikijs-transformers"
            }
        ],
        "color": "default"
    }
},
when it is like an iframe embed, it is
{
    "object": "block",
    "id": "101817d0-5c92-80b2-9e24-d20eb882b801",
    "parent": {
        "type": "page_id",
        "page_id": "85cb68b6-b12f-4a62-af33-3348ea751f77"
    },
    "created_time": "2024-09-14T06:22:00.000Z",
    "last_edited_time": "2024-09-14T06:22:00.000Z",
    "created_by": {
        "object": "user",
        "id": "5aa7609b-54f8-42b7-b4a2-6f38a03279bd"
    },
    "last_edited_by": {
        "object": "user",
        "id": "5aa7609b-54f8-42b7-b4a2-6f38a03279bd"
    },
    "has_children": true,
    "archived": false,
    "in_trash": false,
    "type": "paragraph",
    "paragraph": {
        "rich_text": [
            {
                "type": "text",
                "text": {
                    "content": "Embed paste link as mention: ",
                    "link": null
                },
                "annotations": {
                    "bold": false,
                    "italic": false,
                    "strikethrough": false,
                    "underline": false,
                    "code": false,
                    "color": "default"
                },
                "plain_text": "Embed paste link as mention: ",
                "href": null
            },
            {
                "type": "mention",
                "mention": {
                    "type": "link_mention",
                    "link_mention": {
                        "href": "https://open.spotify.com/playlist/37i9dQZF1DX5KpP2LN299J",
                        "title": "This Is Taylor Swift",
                        "height": 352,
                        "icon_url": "https://open.spotifycdn.com/cdn/images/favicon32.b64ecc03.png",
                        "iframe_url": "https://open.spotify.com/embed/playlist/37i9dQZF1DX5KpP2LN299J?utm_source=oembed",
                        "description": "Playlist · Spotify · 180 items · 6.1M likes",
                        "link_provider": "Spotify",
                        "thumbnail_url": "https://i.scdn.co/image/ab67706f0000000252feef11af8c9d412769ec5a"
                    }
                },
                "annotations": {
                    "bold": false,
                    "italic": false,
                    "strikethrough": false,
                    "underline": false,
                    "code": false,
                    "color": "default"
                },
                "plain_text": "https://open.spotify.com/playlist/37i9dQZF1DX5KpP2LN299J",
                "href": "https://open.spotify.com/playlist/37i9dQZF1DX5KpP2LN299J"
            }
        ],
        "color": "default"
    }
},
popup div for spotify
<div style="display: flex; align-items: center; position: relative; flex-direction: column-reverse; transform-origin: 0% top; left: 0px; top: 6px;">
  <div
    role="dialog"
    style="border-radius: 10px; background: white; backdrop-filter: none; position: relative; max-width: calc(-24px + 100vw); box-shadow: rgba(15, 15, 15, 0.1) 0px 0px 0px 1px, rgba(15, 15, 15, 0.1) 0px 2px 4px; overflow: hidden;"
  >
    <div>
      <div style="width: 340px; max-height: 420px; flex-direction: column; align-items: flex-start;">
        <div style="position: absolute; top: 8px; right: 8px; z-index: 2;">
          <div
            role="button"
            tabindex="0"
            style="user-select: none; transition: background 20ms ease-in; cursor: pointer; display: flex; align-items: center; justify-content: center; width: 24px; height: 24px; border-radius: 4px; fill: rgba(55, 53, 47, 0.35); background: rgba(0, 0, 0, 0.6);"
          >
            <svg
              role="graphics-symbol"
              viewBox="0 0 13 3"
              class="dots"
              style="width: 14px; height: 100%; display: block; fill: white; flex-shrink: 0;"
            >
              <g>
                <path d="M3,1.5A1.5,1.5,0,1,1,1.5,0,1.5,1.5,0,0,1,3,1.5Z"></path>
                <path d="M8,1.5A1.5,1.5,0,1,1,6.5,0,1.5,1.5,0,0,1,8,1.5Z"></path>
                <path d="M13,1.5A1.5,1.5,0,1,1,11.5,0,1.5,1.5,0,0,1,13,1.5Z"></path>
              </g>
            </svg>
          </div>
        </div>
        <div style="width: 340px; height: 100%; aspect-ratio: auto; position: relative;">
          <div style="position: relative; display: flex; justify-content: center; width: 100%; min-height: 100px; height: 352px;">
            <div style="position: absolute; left: 0px; top: 0px; width: 100%; height: 100%; border-radius: 1px;">
              <div style="width: 100%; height: 100%;">
                <div style="position: absolute; left: 0px; top: 0px; width: 100%; height: 100%; border-radius: 1px; pointer-events: auto;">
                  <iframe
                    src="https://open.spotify.com/embed/playlist/37i9dQZF1DX5KpP2LN299J?utm_source=oembed"
                    frameborder="0"
                    sandbox="allow-scripts allow-popups allow-top-navigation-by-user-activation allow-forms allow-same-origin allow-storage-access-by-user-activation allow-popups-to-escape-sandbox"
                    allowfullscreen=""
                    style="position: absolute; left: 0px; top: 0px; width: 100%; height: 100%; border-radius: 1px; pointer-events: auto;"
                  ></iframe>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>;

Somehow this doesn’t work:

const heightClass = Height && Height < 380 ? `h-[${Height}px]` : 'h-[380px]';

but this does

          <div class="w-full min-h-[100px]" style={Height && Height>380?"height:380px":("height:"+Height?.toString()+"px")}>

it is not reading the tailwind h-[${Height}px]. I even tried with toString conversion 🤷‍♀️.

The image stuff

<img src="https://aersf.com/cdn/shop/files/AERL1101_carryon_black_34hero_1200x1200.jpg?v=1715313300" referrerpolicy="same-origin" style="display: block; object-fit: cover; border-radius: 2px; width: 100%; height: 100%; left: 0px; top: calc(50% - 140px);">

Log of I Tried It and It Didn’t Work

I thought maybe I can try changing color of all uploaded icons in dark mode but this doesn’t work
let iconURL = post.Icon && post.Icon.Type === "external" && isNotionIconURL(post.Icon.Url)?post.Icon.Url:null;
//does not work because that needs to be in client side script
if (rootHasDarkClass() && iconURL){iconURL = post.Icon.Url+"?mode=dark";}

So, I am removing this function

export function toggleClass(element: HTMLElement, className: string) {
	element.classList.toggle(className);
}

export function elementHasClass(element: HTMLElement, className: string) {
	return element.classList.contains(className);
}

export function rootHasDarkClass() {
	return elementHasClass(document.documentElement, "dark");
}
export const isNotionIconURL = (url: string): boolean => {
	return !url.startsWith("https://www.notion.so/icons/");
};
export const getIconCssFilter = (iconUrl: string): string | null | undefined => {
	// Regular expression to match the pattern:
	// (1) Any characters after the last underscore
	// (2) Ending with ".svg"
	const regex = /_([^.]+)\.svg$/;
	const match = iconUrl.match(regex);

	// If a match is found, return the captured group, which is the color part.
	// Otherwise, return null if no match is found.
	return match ? match[1] : null;
};
Not supporting reading time/count, so I removed the code related to that
const BLOCKS_FOR_WORD_COUNT: BlockTypes[] = [
	"heading_1",
	"heading_2",
	"heading_3",
	"paragraph",
	"bulleted_list_item",
	"numbered_list_item",
	"to_do",
	"callout",
	"quote",
];

type ContentBlock = {
	content: string;
	children: ContentBlock[];
};
function getSupportedBlocks(blocks: Block[]): ContentBlock[] {
	return blocks
		.filter((block) => BLOCKS_FOR_WORD_COUNT.includes(block.Type))
		.map((block) => {
			if (block.Type === "heading_1" && block.Heading1) {
				return {
					content: block.Heading1.RichTexts.map((text) => text.PlainText).join(" "),
					children: getSupportedBlocks(block.Heading1?.Children || []),
				};
			}
			if (block.Type === "heading_2" && block.Heading2) {
				return {
					content: block.Heading2.RichTexts.map((text) => text.PlainText).join(" "),
					children: getSupportedBlocks(block.Heading2?.Children || []),
				};
			}
			if (block.Type === "heading_3" && block.Heading3) {
				return {
					content: block.Heading3.RichTexts.map((text) => text.PlainText).join(" "),
					children: getSupportedBlocks(block.Heading3?.Children || []),
				};
			}
			if (block.Type === "paragraph" && block.Paragraph) {
				return {
					content: block.Paragraph.RichTexts.map((text) => text.PlainText).join(" "),
					children: getSupportedBlocks(block.Paragraph?.Children || []),
				};
			}
			if (block.Type === "bulleted_list_item" && block.BulletedListItem) {
				return {
					content: block.BulletedListItem.RichTexts.map((text) => text.PlainText).join(" "),
					children: getSupportedBlocks(block.BulletedListItem?.Children || []),
				};
			}
			if (block.Type === "numbered_list_item" && block.NumberedListItem) {
				return {
					content: block.NumberedListItem.RichTexts.map((text) => text.PlainText).join(" "),
					children: getSupportedBlocks(block.NumberedListItem?.Children || []),
				};
			}
			if (block.Type === "to_do" && block.ToDo) {
				return {
					content: block.ToDo.RichTexts.map((text) => text.PlainText).join(" "),
					children: getSupportedBlocks(block.ToDo?.Children || []),
				};
			}
			if (block.Type === "callout" && block.Callout) {
				return {
					content: block.Callout.RichTexts.map((text) => text.PlainText).join(" "),
					children: getSupportedBlocks(block.Callout?.Children || []),
				};
			}
			if (block.Type === "quote" && block.Quote) {
				return {
					content: block.Quote.RichTexts.map((text) => text.PlainText).join(" "),
					children: getSupportedBlocks(block.Quote?.Children || []),
				};
			}

			return { content: "", children: [] };
		});
}

function generateTexts(contentBlocks: ContentBlock[]): string {
	return contentBlocks
		.map((block) => {
			return block.content + "\n" + generateTexts(block.children);
		})
		.join("\n");
}

export function getPageStats(blocks: Block[]) {
	const supportedBlocks = getSupportedBlocks(blocks);
	const pageText = generateTexts(supportedBlocks);

	const wordCount = pageText.split(" ").length;

	return {
		wordCount,
		readTime: `${Math.round((wordCount / 250) * 100) / 100} min`,
	};
}
I am using paginate by astro, so I do not need functions to define content by page number
export async function getPageBySlug(slug: string): Promise<Post | null> {
	const allPosts = await getAllPages();
	return allPosts.find((post) => post.Slug === slug) || null;
}

export async function getPosts(pageSize = 10): Promise<Post[]> {
	const allPosts = await getAllPosts();
	return allPosts.slice(0, pageSize);
}

export async function getRankedPosts(pageSize = 10): Promise<Post[]> {
	const allPosts = await getAllPosts();
	return allPosts
		.filter((post) => !!post.Rank)
		.sort((a, b) => {
			if (a.Rank > b.Rank) {
				return -1;
			} else if (a.Rank === b.Rank) {
				return 0;
			}
			return 1;
		})
		.slice(0, pageSize);
}

export async function getPostsByTag(tagName: string, pageSize = 10): Promise<Post[]> {
	if (!tagName) return [];

	const allPosts = await getAllPosts();
	return allPosts
		.filter((post) => post.Tags.find((tag) => tag.name === tagName))
		.slice(0, pageSize);
}

// page starts from 1 not 0
export async function getPostsByPage(page: number): Promise<Post[]> {
	if (page < 1) {
		return [];
	}

	const allPosts = await getAllPosts();

	const startIndex = (page - 1) * NUMBER_OF_POSTS_PER_PAGE;
	const endIndex = startIndex + NUMBER_OF_POSTS_PER_PAGE;

	return allPosts.slice(startIndex, endIndex);
}

// page starts from 1 not 0
export async function getPostsByTagAndPage(tagName: string, page: number): Promise<Post[]> {
	if (page < 1) {
		return [];
	}

	const allPosts = await getAllPosts();
	const posts = allPosts.filter((post) => post.Tags.find((tag) => tag.name === tagName));

	const startIndex = (page - 1) * NUMBER_OF_POSTS_PER_PAGE;
	const endIndex = startIndex + NUMBER_OF_POSTS_PER_PAGE;

	return posts.slice(startIndex, endIndex);
}

export async function getNumberOfPages(): Promise<number> {
	const allPosts = await getAllPosts();
	return (
		Math.floor(allPosts.length / NUMBER_OF_POSTS_PER_PAGE) +
		(allPosts.length % NUMBER_OF_POSTS_PER_PAGE > 0 ? 1 : 0)
	);
}

export async function getNumberOfPagesByTag(tagName: string): Promise<number> {
	const allPosts = await getAllPosts();
	const posts = allPosts.filter((post) => post.Tags.find((tag) => tag.name === tagName));
	return (
		Math.floor(posts.length / NUMBER_OF_POSTS_PER_PAGE) +
		(posts.length % NUMBER_OF_POSTS_PER_PAGE > 0 ? 1 : 0)
	);
}
Removing stuff from blog-helpers

date formatting is a different file

export const getDateStr = (date: string) => {
	const dt = new Date(date);

	if (date.indexOf("T") !== -1) {
		// Consider timezone
		const elements = date.split("T")[1].split(/([+-])/);
		if (elements.length > 1) {
			const diff = parseInt(`${elements[1]}${elements[2]}`, 10);
			dt.setHours(dt.getHours() + diff);
		}
	}

	const y = dt.getFullYear();
	const m = ("00" + (dt.getMonth() + 1)).slice(-2);
	const d = ("00" + dt.getDate()).slice(-2);
	return y + "-" + m + "-" + d;
};

didn’t use page or tag links

export const getTagLink = (tag: string) => {
	return path.join(BASE_PATH, `/posts/tag/${encodeURIComponent(tag)}`);
};

export const getPageLink = (page: number, tag: string) => {
	if (page === 1) {
		return tag ? getTagLink(tag) : path.join(BASE_PATH, "/");
	}
	return tag
		? path.join(BASE_PATH, `/posts/tag/${encodeURIComponent(tag)}/page/${page.toString()}`)
		: path.join(BASE_PATH, `/posts/page/${page.toString()}`);
};

have 100 functions for file path

export const getStaticFilePath = (pathsup: string): string => {
	return path.join(BASE_PATH, pathsup);
};



Tips