Select the types of activity you want to include in your feed.
feat: add rkey-based routing for publications
Support accessing Standard.site publications via rkey (e.g. /3m3x4bgbsh22k) in addition to slugs (e.g. /blog). Both work interchangeably across all routes including RSS feeds and document paths.
···9090 slug: normalizeSlug(m.slug)
9191 }));
9292}
9393+9494+/**
9595+ * Check if a string is a valid TID (AT Protocol record key)
9696+ *
9797+ * @param str - String to check
9898+ * @returns True if the string matches TID format (12-16 alphanumeric characters)
9999+ */
100100+export function isTidFormat(str: string): boolean {
101101+ const tidPattern = /^[a-zA-Z0-9]{12,16}$/;
102102+ return tidPattern.test(str);
103103+}
104104+105105+/**
106106+ * Get all publication rkeys from slug mappings
107107+ *
108108+ * @returns Array of publication rkeys
109109+ */
110110+export function getAllPublicationRkeys(): string[] {
111111+ return slugMappings.map((m) => m.publicationRkey);
112112+}
+12-3
src/params/slug.ts
···22import { getAllSlugs } from '$lib/config/slugs';
3344/**
55- * Param matcher for valid slugs
66- * Only allows slugs that are defined in the slug-mappings configuration
55+ * Param matcher for valid slugs or publication rkeys
66+ * Allows:
77+ * - Slugs defined in the slug-mappings configuration
88+ * - Publication rkeys (TID format: 12-16 alphanumeric characters)
79 */
810export const match: ParamMatcher = (param) => {
1111+ // Check if it's a configured slug
912 const validSlugs = getAllSlugs();
1010- return validSlugs.includes(param);
1313+ if (validSlugs.includes(param)) {
1414+ return true;
1515+ }
1616+1717+ // Check if it's a valid TID format (AT Protocol record key)
1818+ const tidPattern = /^[a-zA-Z0-9]{12,16}$/;
1919+ return tidPattern.test(param);
1120};
+43-24
src/routes/[slug=slug]/+server.ts
···11import type { RequestHandler } from '@sveltejs/kit';
22import { fetchPublications } from '$lib/services/atproto';
33-import { getPublicationFromSlug } from '$lib/config/slugs';
33+import { getPublicationFromSlug, isTidFormat, getSlugFromPublicationRkey } from '$lib/config/slugs';
4455/**
66- * Dynamic slug root redirect handler
66+ * Dynamic slug/rkey root redirect handler
77 *
88- * Redirects /{slug} to the appropriate Standard.site publication URL
99- * Uses the slug mapping config to find the publication rkey
88+ * Handles both:
99+ * - /{slug} redirects to the appropriate Standard.site publication URL
1010+ * - /{publication-rkey} redirects to the publication (for site.standard.publication rkeys)
1111+ *
1212+ * Uses the slug mapping config to find the publication rkey for slugs.
1013 * Individual documents are handled by the [rkey] route.
1114 */
1215export const GET: RequestHandler = async ({ params, url }) => {
1313- const slug = params.slug;
1616+ const slugOrRkey = params.slug;
14171515- // If there's a path after /{slug}, let it fall through to other routes
1616- const slugPath = url.pathname.replace(new RegExp(`^/${slug}/?`), '');
1818+ // If there's a path after /{slugOrRkey}, let it fall through to other routes
1919+ const slugPath = url.pathname.replace(new RegExp(`^/${slugOrRkey}/?`), '');
17201821 if (slugPath && !['rss', 'atom'].includes(slugPath)) {
1922 // This will be caught by the [rkey] route
···2528 });
2629 }
27302828- // For /{slug} root, redirect to the publication
3131+ // For /{slugOrRkey} root, redirect to the publication
2932 if (!slugPath || slugPath === '') {
3030- // Validate slug and get the publication info
3131- if (!slug) {
3232- return new Response('Invalid slug', {
3333+ // Validate input
3434+ if (!slugOrRkey) {
3535+ return new Response('Invalid slug or rkey', {
3336 status: 400,
3437 headers: {
3538 'Content-Type': 'text/plain; charset=utf-8'
···3740 });
3841 }
39424040- const publicationInfo = getPublicationFromSlug(slug);
4343+ let publicationRkey: string;
4444+ let isDirectRkey = false;
4545+4646+ // Check if input is a TID (rkey) or a slug
4747+ if (isTidFormat(slugOrRkey)) {
4848+ // Input is an rkey - use it directly
4949+ publicationRkey = slugOrRkey;
5050+ isDirectRkey = true;
5151+ } else {
5252+ // Input is a slug - look up the rkey
5353+ const publicationInfo = getPublicationFromSlug(slugOrRkey);
41544242- if (!publicationInfo) {
4343- return new Response(
4444- `Slug not configured: ${slug}\n\nPlease add this slug to src/lib/data/slug-mappings.ts`,
4545- {
4646- status: 404,
4747- headers: {
4848- 'Content-Type': 'text/plain; charset=utf-8'
5555+ if (!publicationInfo) {
5656+ return new Response(
5757+ `Slug not configured: ${slugOrRkey}\n\nPlease add this slug to src/lib/data/slug-mappings.ts`,
5858+ {
5959+ status: 404,
6060+ headers: {
6161+ 'Content-Type': 'text/plain; charset=utf-8'
6262+ }
4963 }
5050- }
5151- );
5252- }
6464+ );
6565+ }
53665454- const { rkey: publicationRkey } = publicationInfo;
6767+ publicationRkey = publicationInfo.rkey;
6868+ }
5569 let redirectUrl: string | null = null;
56705771 try {
···8195 }
82968397 // No publication found
9898+ const identifier = isDirectRkey ? `rkey: ${slugOrRkey}` : `slug: ${slugOrRkey}`;
8499 return new Response(
8585- `Publication not found for slug: ${slug}\n\nPlease check your configuration in src/lib/data/slug-mappings.ts`,
100100+ `Publication not found for ${identifier}\n\n${
101101+ isDirectRkey
102102+ ? 'This publication rkey does not exist or is not accessible.'
103103+ : 'Please check your configuration in src/lib/data/slug-mappings.ts'
104104+ }`,
86105 {
87106 status: 404,
88107 headers: {
+42-27
src/routes/[slug=slug]/[rkey]/+server.ts
···22import { PUBLIC_ATPROTO_DID, PUBLIC_BLOG_FALLBACK_URL } from '$env/static/public';
33import { withFallback } from '$lib/services/atproto';
44import { fetchPublications } from '$lib/services/atproto';
55-import { getPublicationFromSlug } from '$lib/config/slugs';
55+import { getPublicationFromSlug, isTidFormat } from '$lib/config/slugs';
66import type { PublicationPlatform } from '$lib/data/slug-mappings';
7788/**
99- * Smart document redirect handler for slugged publications
99+ * Smart document redirect handler for slugged or rkey-based publications
1010+ *
1111+ * Handles both:
1212+ * - /{slug}/{document-rkey} - publication identified by slug
1313+ * - /{publication-rkey}/{document-rkey} - publication identified by rkey
1014 *
1115 * Automatically detects Standard.site documents and redirects to the canonical URL.
1216 * Uses the publication's URL + document path to construct the final URL.
···8185}
82868387export const GET: RequestHandler = async ({ params, url }) => {
8484- const slug = params.slug;
8585- const rkey = params.rkey;
8888+ const slugOrRkey = params.slug;
8989+ const documentRkey = params.rkey;
86908787- // Validate slug
8888- if (!slug) {
8989- return new Response('Invalid slug', {
9191+ // Validate input
9292+ if (!slugOrRkey) {
9393+ return new Response('Invalid slug or publication rkey', {
9094 status: 400,
9195 headers: {
9296 'Content-Type': 'text/plain; charset=utf-8'
···9498 });
9599 }
961009797- // Get the publication info from the slug
9898- const publicationInfo = getPublicationFromSlug(slug);
101101+ let publicationRkey: string;
102102+ let isDirectRkey = false;
103103+104104+ // Check if input is a TID (rkey) or a slug
105105+ if (isTidFormat(slugOrRkey)) {
106106+ // Input is a publication rkey - use it directly
107107+ publicationRkey = slugOrRkey;
108108+ isDirectRkey = true;
109109+ } else {
110110+ // Input is a slug - look up the publication rkey
111111+ const publicationInfo = getPublicationFromSlug(slugOrRkey);
99112100100- if (!publicationInfo) {
101101- return new Response(
102102- `Slug not configured: ${slug}\n\nPlease add this slug to src/lib/data/slug-mappings.ts`,
103103- {
104104- status: 404,
105105- headers: {
106106- 'Content-Type': 'text/plain; charset=utf-8'
113113+ if (!publicationInfo) {
114114+ return new Response(
115115+ `Slug not configured: ${slugOrRkey}\n\nPlease add this slug to src/lib/data/slug-mappings.ts`,
116116+ {
117117+ status: 404,
118118+ headers: {
119119+ 'Content-Type': 'text/plain; charset=utf-8'
120120+ }
107121 }
108108- }
109109- );
110110- }
122122+ );
123123+ }
111124112112- const { rkey: publicationRkey } = publicationInfo;
125125+ publicationRkey = publicationInfo.rkey;
126126+ }
113127114114- // Validate TID format (AT Protocol record key)
128128+ // Validate document rkey TID format (AT Protocol record key)
115129 const tidPattern = /^[a-zA-Z0-9]{12,16}$/;
116130117117- if (!rkey || !tidPattern.test(rkey)) {
118118- return new Response('Invalid TID format. Expected 12-16 alphanumeric characters.', {
131131+ if (!documentRkey || !tidPattern.test(documentRkey)) {
132132+ return new Response('Invalid document TID format. Expected 12-16 alphanumeric characters.', {
119133 status: 400,
120134 headers: {
121135 'Content-Type': 'text/plain; charset=utf-8'
···124138 }
125139126140 // Detect document and get canonical URL
127127- const detection = await detectDocumentUrl(rkey, publicationRkey);
141141+ const detection = await detectDocumentUrl(documentRkey, publicationRkey);
128142129143 let targetUrl: string | null = null;
130144 let statusCode = 301;
···134148 targetUrl = detection.url;
135149 } else if (PUBLIC_BLOG_FALLBACK_URL) {
136150 // Use fallback URL from environment variable
137137- targetUrl = `${PUBLIC_BLOG_FALLBACK_URL}/${rkey}`;
151151+ targetUrl = `${PUBLIC_BLOG_FALLBACK_URL}/${documentRkey}`;
138152 } else {
139153 // No fallback configured, return 404
154154+ const identifier = isDirectRkey ? `publication rkey "${slugOrRkey}"` : `slug "${slugOrRkey}"`;
140155 return new Response(
141141- `Document not found: ${rkey}
156156+ `Document not found: ${documentRkey}
142157143143-This document could not be found in the Standard.site publication for slug "${slug}".
158158+This document could not be found in the Standard.site publication for ${identifier}.
144159145160Note: Only checking Standard.site publication with rkey: ${publicationRkey}
146161
+23-17
src/routes/[slug=slug]/atom/+server.ts
···11import type { RequestHandler } from '@sveltejs/kit';
22-import { getPublicationRkeyFromSlug } from '$lib/config/slugs';
22+import { getPublicationRkeyFromSlug, isTidFormat } from '$lib/config/slugs';
3344/**
55 * Deprecated Atom feed
66+ *
77+ * Accessible via:
88+ * - /{slug}/atom - publication identified by slug
99+ * - /{publication-rkey}/atom - publication identified by rkey
610 *
711 * Atom feeds are no longer supported. Use RSS instead.
812 *
···1317 * - Maintaining both RSS and Atom adds unnecessary complexity
1418 */
1519export const GET: RequestHandler = ({ params }) => {
1616- const slug = params.slug;
2020+ const slugOrRkey = params.slug;
17211818- // Validate slug
1919- if (!slug) {
2020- return new Response('Invalid slug', {
2222+ // Validate input
2323+ if (!slugOrRkey) {
2424+ return new Response('Invalid slug or publication rkey', {
2125 status: 400,
2226 headers: {
2327 'Content-Type': 'text/plain; charset=utf-8'
···2529 });
2630 }
27312828- // Validate slug exists in config
2929- const publicationRkey = getPublicationRkeyFromSlug(slug);
3232+ // Validate that either the slug exists in config or it's a valid rkey
3333+ if (!isTidFormat(slugOrRkey)) {
3434+ const publicationRkey = getPublicationRkeyFromSlug(slugOrRkey);
30353131- if (!publicationRkey) {
3232- return new Response(
3333- `Slug not configured: ${slug}\n\nPlease add this slug to src/lib/config/slugs.ts`,
3434- {
3535- status: 404,
3636- headers: {
3737- 'Content-Type': 'text/plain; charset=utf-8'
3636+ if (!publicationRkey) {
3737+ return new Response(
3838+ `Slug not configured: ${slugOrRkey}\n\nPlease add this slug to src/lib/config/slugs.ts`,
3939+ {
4040+ status: 404,
4141+ headers: {
4242+ 'Content-Type': 'text/plain; charset=utf-8'
4343+ }
3844 }
3939- }
4040- );
4545+ );
4646+ }
4147 }
42484349 return new Response(
···45514652This Atom feed is no longer available. Please use the RSS feed instead:
47534848-RSS Feed: /${slug}/rss
5454+RSS Feed: /${slugOrRkey}/rss
49555056For Leaflet posts with full content, the RSS feed will automatically redirect you to
5157Leaflet's native RSS feed which includes complete post content.
+34-20
src/routes/[slug=slug]/rss/+server.ts
···66 PUBLIC_SITE_URL
77} from '$env/static/public';
88import { fetchBlogPosts } from '$lib/services/atproto';
99-import { getPublicationRkeyFromSlug } from '$lib/config/slugs';
99+import { getPublicationRkeyFromSlug, isTidFormat } from '$lib/config/slugs';
1010import { generateRSSFeed, createRSSResponse, type RSSItem } from '$lib/utils/rss';
11111212/**
1313- * RSS 2.0 feed for Standard.site publications (accessed via /{slug}/rss)
1313+ * RSS 2.0 feed for Standard.site publications
1414+ *
1515+ * Accessible via:
1616+ * - /{slug}/rss - publication identified by slug
1717+ * - /{publication-rkey}/rss - publication identified by rkey
1418 *
1519 * Generates an RSS feed for all documents in the specified publication.
1620 */
1721export const GET: RequestHandler = async ({ params }) => {
1818- const slug = params.slug;
2222+ const slugOrRkey = params.slug;
19232020- // Validate slug
2121- if (!slug) {
2222- return new Response('Invalid slug', {
2424+ // Validate input
2525+ if (!slugOrRkey) {
2626+ return new Response('Invalid slug or publication rkey', {
2327 status: 400,
2428 headers: {
2529 'Content-Type': 'text/plain; charset=utf-8'
···2731 });
2832 }
29333030- // Get the publication rkey from the slug
3131- const publicationRkey = getPublicationRkeyFromSlug(slug);
3434+ let publicationRkey: string;
32353333- if (!publicationRkey) {
3434- return new Response(
3535- `Slug not configured: ${slug}\n\nPlease add this slug to src/lib/config/slugs.ts`,
3636- {
3737- status: 404,
3838- headers: {
3939- 'Content-Type': 'text/plain; charset=utf-8'
3636+ // Check if input is a TID (rkey) or a slug
3737+ if (isTidFormat(slugOrRkey)) {
3838+ // Input is a publication rkey - use it directly
3939+ publicationRkey = slugOrRkey;
4040+ } else {
4141+ // Input is a slug - look up the publication rkey
4242+ const rkey = getPublicationRkeyFromSlug(slugOrRkey);
4343+4444+ if (!rkey) {
4545+ return new Response(
4646+ `Slug not configured: ${slugOrRkey}\n\nPlease add this slug to src/lib/config/slugs.ts`,
4747+ {
4848+ status: 404,
4949+ headers: {
5050+ 'Content-Type': 'text/plain; charset=utf-8'
5151+ }
4052 }
4141- }
4242- );
5353+ );
5454+ }
5555+5656+ publicationRkey = rkey;
4357 }
44584559 try {
···6579 // Generate RSS feed
6680 const feed = generateRSSFeed(
6781 {
6868- title: `${PUBLIC_SITE_TITLE} - ${slug}`,
8282+ title: `${PUBLIC_SITE_TITLE} - ${slugOrRkey}`,
6983 link: PUBLIC_SITE_URL,
7084 description: PUBLIC_SITE_DESCRIPTION,
7185 language: 'en',
7272- selfLink: `${PUBLIC_SITE_URL}/${slug}/rss`,
8686+ selfLink: `${PUBLIC_SITE_URL}/${slugOrRkey}/rss`,
7387 generator: 'SvelteKit with AT Protocol'
7488 },
7589 items
···7993 }
80948195 // No posts at all
8282- return new Response(`No posts found for publication: ${slug}`, {
9696+ return new Response(`No posts found for publication: ${slugOrRkey}`, {
8397 status: 404,
8498 headers: {
8599 'Content-Type': 'text/plain; charset=utf-8'