Add content to how-i-built-this pages
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 4s

This commit is contained in:
ryan 2025-10-02 16:18:57 -07:00
parent 675849b884
commit ed7cf8a254
10 changed files with 180 additions and 23 deletions

5
hindki/.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,5 @@
{
"files.associations": {
"*.mdx": "markdown"
}
}

View File

@ -86,6 +86,7 @@ export default defineConfig({
{
label: "How I Built This",
autogenerate: { directory: "how-i-built-this" },
collapsed: true,
},
{
label: "Add Vocabulary",

View File

@ -2,7 +2,7 @@
import { Aside, Badge, Icon } from "@astrojs/starlight/components";
import AnchorHeading from "@astrojs/starlight/components/AnchorHeading.astro";
import { render, renderInline, highlight } from "@/lib/markdown";
import type { VocabWord } from "@/types/types";
import type { Tag, VocabWord } from "@/types/types";
const { word } = Astro.props as { word: VocabWord };
const activeTag = Astro.params.tag;
@ -33,6 +33,22 @@ const gender_lookup: Record<"m" | "f", ["note" | "tip", string]> = {
/>
)
}
{
word.tags &&
word.tags
.filter((tag: Tag) => {
return !(
activeTag ||
tag.name == activeTag ||
tag.name == "general"
);
})
.map((tag: Tag) => (
<a class="badge-link" href="/vocabulary/{tag.name}">
<Badge text={tag.name} class="tag-badge" />
</a>
))
}
</AnchorHeading>
{word.note && <div set:html={render(word.note)} />}
{
@ -73,21 +89,8 @@ const gender_lookup: Record<"m" | "f", ["note" | "tip", string]> = {
<b>See also:</b>
{word.seeAlso.map((ref, i) => (
<>
<span set:html={renderInline(ref.reference ?? "")} />{i < word.seeAlso!.length - 1 ? "; " : ""}
</>
))}
</p>
)
}
{
word.tags && (
<p>
{word.tags.filter((tag) => (
!activeTag || tag.name !== activeTag
)).map((tag, i) => (
<>
<Badge text={tag.name} class="tag-badge" />
{i < word.tags!.length - 1 ? " " : ""}
<span set:html={renderInline(ref.reference ?? "")} />
{i < word.seeAlso!.length - 1 ? "; " : ""}
</>
))}
</p>

View File

@ -4,9 +4,4 @@ title: About Me
import { Tabs, TabItem } from "@astrojs/starlight/components";
I'm still deciding how many pages I want to do this way, but this one feels fair enough.
<Tabs syncKey="lang">
<TabItem label="English">My name is Ryan.</TabItem>
<TabItem label="हिंदी">मेरा नाम रायन है.</TabItem>
</Tabs>
My name is Ryan.

View File

@ -0,0 +1,23 @@
---
title: Adding Vocabulary
sidebar:
order: 2
---
I started adding more words using the YAML file, but quickly got a bit annoyed by the repetitiveness of having to type:
```yaml
- type: [part-of-speech]
english: [word]
hindi: [translation]
```
even for words that didn't have more complexity, like examples, notes, references ("see also"), etc.
Pretty much as soon as I popped open my textbook and tried to start entering larger batches of vocabulary -- the list of vocations, for example -- I found myself wanting a more quick flow. Essentially, I was wanting a form.
Since I'm using Astro, I knew I'd be able to add a React component, so I made a page that would modify the yaml file based on a form. But in production, once deployed, I somehow needed to modify the *repository*'s copy of the yaml file, not the one sitting in a docker container.
The first stab was to implement a Github api to edit the file, but this would trigger a ~2 minute redeployment after every word was added, which was needed before the static site generation would regenerate the sidebar and pages.
All of this meant that my actual rate of adding words was pretty slow.

View File

@ -0,0 +1,35 @@
---
title: "Database Backend"
---
Still hoping for minimal complexity, I started with a sqlite file, but as soon as I deployed to production, I realized that file would have the same issue as the YAML file: changes made in production would not persist, and anyway I'd need to re-deploy the site after every word.
Now, I've implemented a postgres backend.
## Migration
I already had a postgres cluster on my VPS, so all I had to do was add a user and database for Hindki and hook it up.
In the initial sqlite migration, I had already implemented Drizzle ORM to interface between a database and my app, and because I had started with YAML, I already had a good sense of the schema I wanted (often my pain point with database-driven apps, since I end up with a ton of migrations as I change my mind about data architecture while building).
In the migration, I changed the rigid categories I had been using to tags, which are much more flexible.
Previously:
```yaml
- slug: food-and-drink
about: Words about food and drinks.
words:
- english: to eat
hindi: खाना
- ...
- ...
```
This was kind of bothering me, actually, because a given word might belong in multiple categories, which created a bit of friction when adding words. With tags, this is now possible.
## How it works now
My vocabulary "page" is just one file, `[tag].astro`, which uses the storage backend to pull the list of words with tag `tag` dynamically from the api, parses the list to generate the right-hand sidebar to organize by parts of speech (I've just called these "types" with the expectation that in some categories, I'll want to organize words by more interesting dilineations), and then renders a component, `VocabWord.astro`, that looks identical to the textbook-like look I had initially set up when the site was just a static site generator using YAML.
Finally, I created a custom sidebar overriding Starlight/Astro's default, which also dynamically pulls the list of tags by fetching `/api/tags.json` so that when a word is added in the web form, the sidebar _and_ content both immediately update.
Essentially, I was able to move the data to postgres -- and access the same database from both dev and prod -- while maintaining the user experience perfectly!

View File

@ -1,3 +1,34 @@
---
title: Deployment
---
The site is deployed on Coolify, a self-hosted Platform-As-A-Service running on my VPS.
Coolify internally uses Caddy as a proxy, so any projects/apps I deploy automatically get a subdomain with https.
For continuous deployment, I have set up a webhook, so that as soon as a commit is pushed to the repository (which is only necessary for code changes now, not for vocabulary updates), a rebuild is automatically triggered. Within about 90 seconds, the site updates to the new version.
If there are any errors in deployment, it just reverts to the previous working deploy.
For posterity, here's the Dockerfile that allows this Astro/Starlight project to build:
```dockerfile
FROM node:lts AS runtime
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
ENV HOST=0.0.0.0
ENV PORT=4321
EXPOSE 4321
# DATABASE_URL will be provided as an environment variable at runtime
# Set in Coolify or via docker run -e DATABASE_URL=...
CMD node ./dist/server/entry.mjs
```

View File

@ -0,0 +1,51 @@
---
title: Flash Cards / Learning Mode
sidebar:
order: 2
---
With a starter-pack of vocabulary (around 75 words) on the site, I tried putting together a "learning mode" with flash cards.
This is, in a way, the reason I started this thing in the first place: to reinvent Anki in a way that gives me more control over the cards, a better user experience, and also outputs a "book" with everything I've learned about Hindi.
So, I made another React component under `/learn`.
I hit a snag between server-side and client-side rendering (obviously -- anyone who saw me start this project as a static site and then try to implement dynamic data probably saw this coming). I wanted to be able to "live" filter the words that were being generated as flash cards. My solution is as follows.
## Gluing the dynamic page to Astro
On `pages/learn/index.astro`, the actual Astro file is pretty basic, essentially just bootstrapping the React component:
```astro
---
import StarlightPage from "@astrojs/starlight/components/StarlightPage.astro";
import FlashCardActivity from "@/components/FlashCardActivity";
export const prerender = false;
---
<StarlightPage
frontmatter={{
title: "Flash Cards",
tableOfContents: true,
prev: false,
}}
>
<FlashCardActivity client:load />
</StarlightPage>
```
## Basic flash card functionality
[`FlashCardActivity.tsx`](https://git.ryanpandya.com/ryan/hindki/src/branch/main/hindki/src/components/FlashCardActivity.tsx) does the following:
- Set up stateful React variables to keep track of the words currently in the flash card "pool", and the filters that are currently selected
- Use the json api to pull the current collection of words and tags
- Render the overall flash card activity -- basically just some headings, the checkboxes for filters, and finally, a `<FlashCard />` component to actually show the flash card.
The Flash Card component mainly just takes in the word (with associated translation, and any example sentences), creates a few variants (english -> hindi; hindi -> english, and the same pair for each example sentence); then renders a stylized box with a button to flip the card over. Obviously, some React variables keep track of the card's state.
That's about it for the basic functionality of a flash card.
## Spaced repetition
It quickly became clear that I needed a way to mark some cards as "learned" and get them out of the deck. This is the principle of spaced repetition (how Anki works): when a card comes up, you mark it as either easy, medium, or hard (basically); then, based on this and the last time the card was seen (and maybe some other factors I haven't looked up yet), the card is given a higher or lower likelihood to show up again.
In effect, the result is that more recently learned *or* harder words show up more frequently; and super easy words show up very infrequently, so that you are still occasionally quizzed on them (because if they just disappeared when you "first" learn them, you'd probably forget them eventually).

View File

@ -1,5 +1,7 @@
---
title: "Hindki Architecture Overview"
sidebar:
order: 1
---
One could certainly argue that Hindki is as much about procrastinating learning Hindi by setting up an elaborate website as it is, about, well, learning Hindi.
@ -9,3 +11,5 @@ The major decisions that led to this site, and are elaborated in the next few pa
- Using Astro, the static site generator, to (mostly) focus on ease of adding content. I'm using Starlight, which is basically a theme for Astro that comes with some components, sane defaults, and probably more that I'm not taking advantage of.
- Overriding the fonts to look more like how I would typeset a book using (Xe)LaTeX.
- Keeping track of vocabulary in a YAML file, and generating the vocabulary pages quasi-dynamically during the build phase of static site generation. In other words, I didn't want to have to hand-write each page (because I kept changing my mind about how the entries would look, and it quickly became clear that I should use reusable components), but I also didn't want to use a heavy/full-featured CRUD backend, because at the end of the day, this site is _just content_.
- Realizing that even though it's _just content_, most of that content is actually structured data with a schema, so a web form makes sense to lubricate adding vocabulary easily.
- Realizing that thanks to my particular use case of wanting to edit the live production database both from the website, as well as from my development laptop and desktop, I should just implement a database backend.

View File

@ -224,7 +224,7 @@ h6,
font-weight: 400;
}
.gender-badge {
.sl-badge {
margin-left: 0.5rem;
vertical-align: middle;
font-family: var(--font-sans);
@ -236,6 +236,15 @@ h6,
color: var(--sl-color-text);
}
.tag-badge:hover {
background: var(--sl-color-accent);
color: var(--sl-color-bg);
}
.sl-markdown-content a.badge-link:hover {
text-decoration: none;
}
.word-entry {
margin-bottom: 2em;
}