Skip to content

Commit

Permalink
feat(page): use dns entries for custom domains
Browse files Browse the repository at this point in the history
  • Loading branch information
alexander-heimbuch committed Jan 11, 2025
1 parent bc30fbd commit a131f51
Show file tree
Hide file tree
Showing 19 changed files with 286 additions and 131 deletions.
7 changes: 5 additions & 2 deletions apps/page/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@
"ndarray": "1.0.19",
"quantize": "1.0.2",
"@heroicons/vue": "2.2.0",
"sanitize-html": "2.14.0"
"sanitize-html": "2.14.0",
"@layered/dns-records": "2.1.0",
"tldts": "6.1.71"
},
"devDependencies": {
"@types/lodash-es": "4.17.12",
Expand All @@ -50,6 +52,7 @@
"@types/get-pixels": "3.3.4",
"@types/quantize": "1.0.2",
"@types/ndarray": "1.0.14",
"@types/sanitize-html": "2.13.0"
"@types/sanitize-html": "2.13.0",
"@cloudflare/workers-types": "4.20250109.0"
}
}
11 changes: 11 additions & 0 deletions apps/page/src/env.d.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,13 @@
/// <reference path="../.astro/types.d.ts" />
/// <reference types="astro/client" />

type KVNamespace = import("@cloudflare/workers-types").KVNamespace;
type ENV = {
CUSTOM_DOMAINS: KVNamespace;
};

// use a default runtime configuration (advanced mode).
type Runtime = import("@astrojs/cloudflare").Runtime<ENV>;
declare namespace App {
interface Locals extends Runtime {}
}
3 changes: 2 additions & 1 deletion apps/page/src/layouts/Layout.astro
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import Colors from '../features/Colors.vue';
import { store } from '../logic';
import { getLanguage } from '../i18n';
const { title, description, favicon } = Astro.props;
const { title, description, favicon, baseUrl } = Astro.props;
const state = store.getState();
const lang = getLanguage();
---
Expand All @@ -25,6 +25,7 @@ const lang = getLanguage();
<meta name="generator" content="Podlove Lux" />
<title>{title}</title>
<meta name="description" content={description} />
<base href={baseUrl} />
<script is:inline define:vars={{ state }}>
window.REDUX_STATE = state;
</script>
Expand Down
30 changes: 30 additions & 0 deletions apps/page/src/lib/dns-record.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { getDnsRecords } from '@layered/dns-records';
import { get, noop } from 'lodash-es';
import type { APIContext } from 'astro';
import { getDomain } from 'tldts';
import { safeParse } from './json';

type FeedData = { feed: string | null; primary_color: string | null };

const getStore = (context: APIContext): KVNamespace =>
get(context, ['locals', 'runtime', 'env', 'CUSTOM_DOMAINS'], {
get: async () => null,
put: noop
} as unknown as KVNamespace);

export const extractDnsData = async (context: APIContext): Promise<FeedData> => {
const domain = getDomain(context.url.hostname);
const entryName = `lux.${domain}`;
const store = getStore(context);

let result = await store.get(entryName);

if (!result) {
const dnsEntry = await getDnsRecords(entryName, 'TXT');
result = get(dnsEntry, ['data', 0], null);
result && store.put(entryName, result, { expirationTtl: 60 * 60 });
}

const fallback = { feed: null, primary_color: null };
return safeParse<FeedData>(result || JSON.stringify(fallback), fallback);
};
7 changes: 7 additions & 0 deletions apps/page/src/lib/json.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const safeParse = <T>(input: string, fallback: T) => {
try {
return JSON.parse(input);
} catch (err) {
return fallback;
}
}
26 changes: 18 additions & 8 deletions apps/page/src/logic/store/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,19 +66,19 @@ const chaptersImage = createSelector(slices.player, player.chaptersImage);

// router
const base = createSelector(slices.router, router.base);

const customDomain = createSelector(slices.router, router.customDomain);
const translation = (key: string, attr = {}) => ({ key, attr });

export default {
initialized: (state: State) => {
return state.theme.initialized
return state.theme.initialized;
},
runtime: {
initialized: createSelector(slices.runtime, runtime.initialized),
locale: createSelector(slices.runtime, runtime.locale),
cacheKey,
buildDate: createSelector(slices.runtime, runtime.buildDate),
version: createSelector(slices.runtime, runtime.version),
version: createSelector(slices.runtime, runtime.version)
},
podcast: {
show: createSelector(slices.podcast, podcast.show),
Expand Down Expand Up @@ -118,7 +118,7 @@ export default {
},
theme: {
colors: createSelector(slices.theme, theme.colors),
initialized: createSelector(slices.theme, theme.initialized),
initialized: createSelector(slices.theme, theme.initialized)
},
show: {
poster: showPoster,
Expand Down Expand Up @@ -238,11 +238,21 @@ export default {
router: {
base,
episodeId: createSelector(slices.router, router.episodeId),
index: createSelector([base, feed], (...args) => args.filter(Boolean).join('/')),
index: createSelector([base, feed, customDomain], (base, feed, customDomain) => {
if (customDomain) {
return '/';
}

return [base, feed].filter(Boolean).join('/');
}),
episode: (episodeId: string) =>
createSelector([base, feed], (...args) =>
[...args, 'episode', episodeId].filter(Boolean).join('/')
)
createSelector([base, feed, customDomain], (base, feed, customDomain) => {
if (customDomain) {
return ['episode', episodeId].filter(Boolean).join('/');
}

return [base, feed, 'episode', episodeId].filter(Boolean).join('/');
})
},
a11y: {
chapterNext: (state: State) => {
Expand Down
11 changes: 9 additions & 2 deletions apps/page/src/logic/store/stores/router.store.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@

import { last } from 'lodash-es';
import { createAction, handleActions, type Action } from 'redux-actions';
import { actions as runtimeActions } from "./runtime.store";

export interface State {
path: string[];
customDomain: boolean;
}

export type navigatePayload = string[];
Expand All @@ -26,10 +28,14 @@ const updatePath = (state: State, { payload }: Action<string[]>) => ({

export const reducer = handleActions<State, any>(
{
[runtimeActions.initializeApp.toString()]: (state, action: ReturnType<typeof runtimeActions.initializeApp>) => ({
...state,
customDomain: action.payload.customDomain
}),
[actions.navigate.toString()]: updatePath,
[actions.setRoute.toString()]: updatePath
},
{ path: [] }
{ path: [], customDomain: true }
);

export const selectors = {
Expand Down Expand Up @@ -57,5 +63,6 @@ export const selectors = {
default:
return null;
}
}
},
customDomain: (state: State) => state.customDomain
};
1 change: 1 addition & 0 deletions apps/page/src/logic/store/stores/runtime.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export interface initializeAppPayload {
feed: string;
locale: string;
episodeId?: number;
customDomain: boolean;
}

export type dataFetchedPayload = {
Expand Down
20 changes: 20 additions & 0 deletions apps/page/src/middleware/custom-domain.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { defineMiddleware } from "astro:middleware";
import { getRequestParams } from "./request-param";

export const handleCustomDomain = defineMiddleware(async ({ request, rewrite, originPathname }, next) => {
const { feed, episodeId } = getRequestParams(request);

if (feed && episodeId && originPathname !== `/feed/${feed}/episode/${episodeId}`) {
return rewrite(`/feed/${feed}/episode/${episodeId}`);
}

if (feed && originPathname !== '/feed') {
return rewrite('/feed');
}

if (!feed && originPathname !== '/search') {
return rewrite('/search');
}

return next();
});
14 changes: 9 additions & 5 deletions apps/page/src/middleware/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@ import { sequence } from 'astro:middleware';

import { initializeStore } from './store';
import { setEtag } from './caching';
import { defineMiddlewareRouter } from './router'
import { defineMiddlewareRouter } from './router';
import { extractRequestParams } from './request-param';
import { handleCustomDomain } from './custom-domain';

export const onRequest = defineMiddlewareRouter({
'/feed/**': sequence(initializeStore, setEtag),
'/proxy**': sequence()
})
export const onRequest = defineMiddlewareRouter([
['/feed/**', sequence(extractRequestParams, initializeStore, setEtag)],
['/api/**', sequence()],
['/search**', sequence()],
['/**', sequence(extractRequestParams, handleCustomDomain, initializeStore, setEtag)]
]);
34 changes: 34 additions & 0 deletions apps/page/src/middleware/request-param.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { defineMiddleware } from 'astro:middleware';
import { get } from 'lodash-es';
import { extractDnsData } from '../lib/dns-record';


export const getRequestParams = (request: Request): { feed: string | undefined; episodeId: string | undefined; customDomain: boolean; } => {
const { feed, episodeId, customDomain } = get(request, 'data') as unknown as {
feed: string;
episodeId: string;
customDomain: boolean;
};

return {
feed,
episodeId,
customDomain
}
};

export const extractRequestParams = defineMiddleware(async (context, next) => {
const { request, params } = context;
let { feed, episodeId } = params;

const dns = await extractDnsData(context);

if (dns.feed) {
feed = dns.feed;
}

(request as any).data = { feed, episodeId, customDomain: !!dns.feed };

return next();
});

24 changes: 14 additions & 10 deletions apps/page/src/middleware/router.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import { defineMiddleware, sequence } from 'astro:middleware';
import type { MiddlewareHandler } from 'astro';
import { defineMiddleware } from 'astro:middleware';
import multimatch from 'multimatch';

export function defineMiddlewareRouter(router: Record<string, any>) {
const entries = Object.entries(router);
return defineMiddleware((context, next) =>
sequence(
...entries
.filter(([path]) => multimatch(context.url.pathname, path).length > 0)
.map(([_, handler]) => handler)
)(context, next)
);
export function defineMiddlewareRouter(entries: [string, MiddlewareHandler][]): MiddlewareHandler {
return defineMiddleware((context, next) => {
const match = entries.find(([path]) => multimatch(context.url.pathname, path).length > 0);

if (!match) {
return next();
}

const [, routeHandler] = match;

return routeHandler(context, next);
});
}
18 changes: 12 additions & 6 deletions apps/page/src/middleware/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,26 @@ import { getRequestHeader } from '../lib/middleware';
import parseFeed from '../logic/data/feed-parser';
import type { Podcast } from '../types/feed.types';
import { createHash } from '../lib/caching';
import { getRequestParams } from './request-param';

const version = import.meta.env.VITE_COMMIT_HASH;

console.log({ version })

export const initializeStore = defineMiddleware(async ({ request, params }, next) => {
export const initializeStore = defineMiddleware(async ({ request }, next) => {
const locale = getRequestHeader(request, 'accept-language', 'en-US');
const { feed, episodeId } = params;
const { feed, episodeId, customDomain } = getRequestParams(request);

if (!feed) {
throw Error('Missing Feed Url');
throw new Error('Missing Feed');
}

store.dispatch(actions.lifecycle.initializeApp({ feed, locale, episodeId: toInteger(episodeId) }));
store.dispatch(
actions.lifecycle.initializeApp({
feed,
locale,
episodeId: toInteger(episodeId),
customDomain
})
);

const data: Podcast = await parseFeed({ feed, episodeId: toInteger(episodeId) });
const cacheKey: string | null = data.etag ? await createHash(`${data.etag}${version}`) : null;
Expand Down
2 changes: 2 additions & 0 deletions apps/page/src/pages/episode/[episodeId].astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
11 changes: 6 additions & 5 deletions apps/page/src/pages/feed/[...feed]/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,18 @@ import { actions, selectors, store } from '../../../logic';
import HeroIndex from '../../../screens/archive/Hero.vue';
import LoadMore from '../../../screens/archive/LoadMore.vue';
const baseUrl = Astro.originPathname;
const state = store.getState();
const title = selectors.podcast.title(state);
const favicon = selectors.podcast.poster(state);
const description = selectors.podcast.description(state);
store.dispatch(actions.router.setRoute(['feed']));
---
<Layout title={title} favicon={favicon} description={description}>
<HeroIndex />

<Layout title={title} favicon={favicon} description={description} baseUrl={baseUrl}>
<HeroIndex />

<div class="flex justify-center">
<EpisodeList client:idle />
Expand All @@ -22,6 +25,4 @@ store.dispatch(actions.router.setRoute(['feed']));
<LoadMore class="flex justify-center mb-8" client:idle />
</Layout>

<style>

</style>
<style></style>
Loading

0 comments on commit a131f51

Please sign in to comment.