···2525 }
26262727 // Fall back to checking raw record for legacy base_path
2828- if (
2929- isLeafletPublication(pub.record) &&
3030- pub.record.base_path &&
3131- isProductionDomain()
3232- ) {
2828+ if (isLeafletPublication(pub.record) && pub.record.base_path && isProductionDomain()) {
3329 return `https://${pub.record.base_path}`;
3430 }
3531···4036 const normalized = normalizePublicationRecord(pub.record);
4137 const aturi = new AtUri(pub.uri);
42384343- //use rkey, fallback to name
4444- const name = aturi.rkey || normalized?.name;
3939+ // Use normalized name if available, fall back to rkey
4040+ const name = normalized?.name || aturi.rkey;
4541 return `/lish/${aturi.host}/${encodeURIComponent(name || "")}`;
4642}
···1414 */
15151616import type * as PubLeafletDocument from "../api/types/pub/leaflet/document";
1717-import * as PubLeafletPublication from "../api/types/pub/leaflet/publication";
1717+import type * as PubLeafletPublication from "../api/types/pub/leaflet/publication";
1818import type * as PubLeafletContent from "../api/types/pub/leaflet/content";
1919import type * as SiteStandardDocument from "../api/types/site/standard/document";
2020import type * as SiteStandardPublication from "../api/types/site/standard/publication";
···3131};
32323333// Normalized publication type - uses the generated site.standard.publication type
3434-// with the theme narrowed to only the valid pub.leaflet.publication#theme type
3535-// (isTheme validates that $type is present, so we use $Typed)
3636-// Note: We explicitly list fields rather than using Omit because the generated Record type
3737-// has an index signature [k: string]: unknown that interferes with property typing
3838-export type NormalizedPublication = {
3939- $type: "site.standard.publication";
4040- name: string;
4141- url: string;
4242- description?: string;
4343- icon?: SiteStandardPublication.Record["icon"];
4444- basicTheme?: SiteStandardThemeBasic.Main;
4545- theme?: $Typed<PubLeafletPublication.Theme>;
4646- preferences?: SiteStandardPublication.Preferences;
4747-};
3434+export type NormalizedPublication = SiteStandardPublication.Record;
48354936/**
5037 * Checks if the record is a pub.leaflet.document
···223210): NormalizedPublication | null {
224211 if (!record || typeof record !== "object") return null;
225212226226- // Pass through site.standard records directly, but validate the theme
213213+ // Pass through site.standard records directly
227214 if (isStandardPublication(record)) {
228228- // Validate theme - only keep if it's a valid pub.leaflet.publication#theme
229229- const theme = PubLeafletPublication.isTheme(record.theme)
230230- ? (record.theme as $Typed<PubLeafletPublication.Theme>)
231231- : undefined;
232232- return {
233233- ...record,
234234- theme,
235235- };
215215+ return record;
236216 }
237217238218 if (isLeafletPublication(record)) {
···245225246226 const basicTheme = leafletThemeToBasicTheme(record.theme);
247227248248- // Validate theme - only keep if it's a valid pub.leaflet.publication#theme with $type set
249249- // For legacy records without $type, add it during normalization
250250- let theme: $Typed<PubLeafletPublication.Theme> | undefined;
251251- if (record.theme) {
252252- if (PubLeafletPublication.isTheme(record.theme)) {
253253- theme = record.theme as $Typed<PubLeafletPublication.Theme>;
254254- } else {
255255- // Legacy theme without $type - add it
256256- theme = {
257257- ...record.theme,
258258- $type: "pub.leaflet.publication#theme",
259259- };
260260- }
261261- }
262262-263228 // Convert preferences to site.standard format (strip/replace $type)
264229 const preferences: SiteStandardPublication.Preferences | undefined =
265230 record.preferences
···278243 description: record.description,
279244 icon: record.icon,
280245 basicTheme,
281281- theme,
246246+ theme: record.theme,
282247 preferences,
283248 };
284249 }
···11--- Add sort_date computed column to documents table
22--- This column stores the older of publishedAt (from JSON data) or indexed_at
33--- Used for sorting feeds chronologically by when content was actually published
44-55--- Create an immutable function to parse ISO 8601 timestamps from text
66--- This is needed because direct ::timestamp cast is not immutable (accepts 'now', 'today', etc.)
77--- The regex validates the format before casting to ensure immutability
88-CREATE OR REPLACE FUNCTION parse_iso_timestamp(text) RETURNS timestamptz
99-LANGUAGE sql IMMUTABLE STRICT AS $$
1010- SELECT CASE
1111- -- Match ISO 8601 format: YYYY-MM-DDTHH:MM:SS with optional fractional seconds and Z/timezone
1212- WHEN $1 ~ '^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:?\d{2})?$' THEN
1313- $1::timestamptz
1414- ELSE
1515- NULL
1616- END
1717-$$;
1818-1919-ALTER TABLE documents
2020-ADD COLUMN sort_date timestamptz GENERATED ALWAYS AS (
2121- LEAST(
2222- COALESCE(parse_iso_timestamp(data->>'publishedAt'), indexed_at),
2323- indexed_at
2424- )
2525-) STORED;
2626-2727--- Create index on sort_date for efficient ordering
2828-CREATE INDEX documents_sort_date_idx ON documents (sort_date DESC, uri DESC);