feat: add optional slug to public note URLs

This commit is contained in:
Julien Calixte
2026-02-17 13:38:45 +01:00
parent c4dd418cd8
commit 5ea5db2655
5 changed files with 27 additions and 4 deletions

View File

@@ -31,7 +31,7 @@ const routes: Array<RouteRecordRaw> = [
component: () => import("@/views/PublicNoteListByDidView.vue"), component: () => import("@/views/PublicNoteListByDidView.vue"),
}, },
{ {
path: "/pub/:did/:rkey", path: "/pub/:did/:rkey/:slug?",
name: "PublicNoteView", name: "PublicNoteView",
props: true, props: true,
component: () => import("@/views/PublicNoteView.vue"), component: () => import("@/views/PublicNoteView.vue"),

9
src/utils/slugify.ts Normal file
View File

@@ -0,0 +1,9 @@
export function slugify(text: string): string {
return text
.toLowerCase()
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "")
.replace(/[^a-z0-9]+/g, "-")
.replace(/-+/g, "-")
.replace(/^-|-$/g, "")
}

View File

@@ -2,6 +2,7 @@
import BackButton from "@/components/BackButton.vue" import BackButton from "@/components/BackButton.vue"
import { usePublicNoteList } from "@/hooks/usePublicNoteList.hook" import { usePublicNoteList } from "@/hooks/usePublicNoteList.hook"
import { getAuthor } from "@/modules/atproto/getAuthor" import { getAuthor } from "@/modules/atproto/getAuthor"
import { slugify } from "@/utils/slugify"
import { computedAsync } from "@vueuse/core" import { computedAsync } from "@vueuse/core"
import { computed } from "vue" import { computed } from "vue"
import { vInfiniteScroll } from "@vueuse/components" import { vInfiniteScroll } from "@vueuse/components"
@@ -31,7 +32,7 @@ const author = computedAsync(async () => getAuthor(did.value))
<router-link <router-link
:to="{ :to="{
name: 'PublicNoteView', name: 'PublicNoteView',
params: { did: note.did, rkey: note.rkey }, params: { did: note.did, rkey: note.rkey, slug: slugify(note.title) },
}" }"
class="btn btn-link" class="btn btn-link"
>{{ note.title }}</router-link >{{ note.title }}</router-link

View File

@@ -1,6 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import BackButton from "@/components/BackButton.vue" import BackButton from "@/components/BackButton.vue"
import { usePublicNoteList } from "@/hooks/usePublicNoteList.hook" import { usePublicNoteList } from "@/hooks/usePublicNoteList.hook"
import { slugify } from "@/utils/slugify"
import { vInfiniteScroll } from "@vueuse/components" import { vInfiniteScroll } from "@vueuse/components"
const { notes, isLoading, canLoadMore, onLoadMore, getAuthor } = const { notes, isLoading, canLoadMore, onLoadMore, getAuthor } =
@@ -24,7 +25,7 @@ const { notes, isLoading, canLoadMore, onLoadMore, getAuthor } =
<router-link <router-link
:to="{ :to="{
name: 'PublicNoteView', name: 'PublicNoteView',
params: { did: note.did, rkey: note.rkey }, params: { did: note.did, rkey: note.rkey, slug: slugify(note.title) },
}" }"
class="btn btn-link" class="btn btn-link"
>{{ note.title }}</router-link >{{ note.title }}</router-link

View File

@@ -9,12 +9,15 @@ import type { PublicNoteRecord } from "@/modules/atproto/publicNote.types"
import { withATProtoImages } from "@/modules/atproto/withATProtoImages" import { withATProtoImages } from "@/modules/atproto/withATProtoImages"
import { getUrl } from "@/modules/atproto/getUrl" import { getUrl } from "@/modules/atproto/getUrl"
import { downloadFont } from "@/utils/downloadFont" import { downloadFont } from "@/utils/downloadFont"
import { slugify } from "@/utils/slugify"
import { computedAsync } from "@vueuse/core" import { computedAsync } from "@vueuse/core"
import { computed, nextTick, watch } from "vue" import { computed, nextTick, watch } from "vue"
import { useRouter } from "vue-router"
import { useResizeContainer } from "@/hooks/useResizeContainer.hook" import { useResizeContainer } from "@/hooks/useResizeContainer.hook"
import ThemeSwap from "@/components/ThemeSwap.vue" import ThemeSwap from "@/components/ThemeSwap.vue"
const props = defineProps<{ did: string; rkey: string }>() const props = defineProps<{ did: string; rkey: string; slug?: string }>()
const router = useRouter()
const did = computed(() => props.did) const did = computed(() => props.did)
const rkey = computed(() => props.rkey) const rkey = computed(() => props.rkey)
@@ -31,6 +34,15 @@ const noteRecord = computedAsync(async () =>
) )
watch(noteRecord, () => { watch(noteRecord, () => {
if (
noteRecord.value?.value.title &&
props.slug &&
props.slug !== slugify(noteRecord.value.value.title)
) {
router.replace({ name: "SpaceCowboy" })
return
}
if (noteRecord.value?.value.fontFamily) { if (noteRecord.value?.value.fontFamily) {
downloadFont(noteRecord.value.value.fontFamily) downloadFont(noteRecord.value.value.fontFamily)
} }