├── netlify.toml ├── .npmrc ├── public ├── cover.jpg ├── banner.webp ├── audio │ └── .gitattributes ├── favicon.svg └── cmsBanner.svg ├── src ├── env.d.ts ├── config.ts ├── pages │ ├── about.astro │ ├── 404.astro │ ├── [...page].astro │ ├── episode │ │ └── [slug].astro │ └── rss.xml.js ├── components │ ├── icons │ │ ├── pocketcastsIcon.astro │ │ ├── playLineIcon.astro │ │ ├── telegramIcon.astro │ │ ├── deezerIcon.astro │ │ ├── heartIcon.astro │ │ ├── facebookIcon.astro │ │ ├── newsletterIcon.astro │ │ ├── youtubeIcon.astro │ │ ├── ivooxIcon.astro │ │ ├── pauseLineIcon.astro │ │ ├── stopLineIcon.astro │ │ ├── instagramIcon.astro │ │ ├── downloadIcon.astro │ │ ├── playIcon.astro │ │ ├── rssIcon.astro │ │ ├── spotifyIcon.astro │ │ ├── externalLinkIcon.astro │ │ ├── audioIcon.astro │ │ ├── linkedinIcon.astro │ │ ├── applePodcastIcon.astro │ │ ├── githubIcon.astro │ │ ├── googlePodcastIcon.astro │ │ ├── headphoneIcon.astro │ │ ├── twitterIcon.astro │ │ ├── pauseIcon.astro │ │ ├── audioMutedIcon.astro │ │ ├── acastPodcastsIcon.astro │ │ ├── fountainIcon.astro │ │ ├── amazonMusicIcon.astro │ │ └── easyPodcastIcon.astro │ ├── Footer.astro │ ├── pagination.astro │ ├── BaseHead.astro │ ├── headerCard │ │ ├── followUsMenu.astro │ │ ├── listenOnMenu.astro │ │ └── HeaderCard.astro │ ├── HorizontalCard.astro │ └── audioPlayer.astro ├── content │ ├── episode │ │ └── post1.md │ └── config.ts ├── layouts │ ├── BaseLayout.astro │ └── EpisodeLayout.astro └── helpers │ ├── podcastMainCategories.json │ ├── themes.json │ └── languagesList.json ├── tsconfig.json ├── .astropod ├── social.config.json ├── listenOn.config.json └── astropod.config.json ├── .gitignore ├── tailwind.config.cjs ├── .github └── FUNDING.yml ├── package.json ├── astro.config.mjs ├── LICENSE ├── README.md └── decap.config.mjs /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | publish = "dist/" 3 | command = "pnpm run build" -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | # Expose Astro dependencies for `pnpm` users 2 | shamefully-hoist=true 3 | -------------------------------------------------------------------------------- /public/cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manuelernestog/astropod/HEAD/public/cover.jpg -------------------------------------------------------------------------------- /public/banner.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manuelernestog/astropod/HEAD/public/banner.webp -------------------------------------------------------------------------------- /src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "astro/tsconfigs/base", 3 | "compilerOptions": { 4 | "strictNullChecks": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | export const SITE_TITLE = 'Astropod | Host your podcast for free'; 2 | export const SITE_DESCRIPTION = 'Astropod is a free and open-source template for host podcast using GitHub and GitHub-Pages.'; 3 | -------------------------------------------------------------------------------- /.astropod/social.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "facebook": "", 3 | "twitter": "", 4 | "youtube": "", 5 | "instagram": "", 6 | "github": "", 7 | "telegram": "", 8 | "linkedin": "", 9 | "newsletter": "" 10 | } -------------------------------------------------------------------------------- /public/audio/.gitattributes: -------------------------------------------------------------------------------- 1 | *.mp3 filter=lfs diff=lfs merge=lfs -text 2 | *.m4a filter=lfs diff=lfs merge=lfs -text 3 | *.ogg filter=lfs diff=lfs merge=lfs -text 4 | *.aac filter=lfs diff=lfs merge=lfs -text 5 | *.wav filter=lfs diff=lfs merge=lfs -text 6 | -------------------------------------------------------------------------------- /.astropod/listenOn.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "spotify": "", 3 | "googlePodcast": "", 4 | "applePodcast": "", 5 | "easyPodcast": "", 6 | "amazonMusic": "", 7 | "fountain": "", 8 | "ivoox": "", 9 | "pocketcasts": "", 10 | "deezer": "", 11 | "acastPodcasts": "" 12 | } 13 | -------------------------------------------------------------------------------- /src/pages/about.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import EpisodeLayout from "../layouts/EpisodeLayout.astro"; 3 | import astropodConfig from "../../.astropod/astropod.config.json"; 4 | --- 5 | 6 | 7 | {astropodConfig.description} 8 | 9 | -------------------------------------------------------------------------------- /src/components/icons/pocketcastsIcon.astro: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # build output 2 | dist/ 3 | .cache 4 | # dependencies 5 | node_modules/ 6 | 7 | # logs 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | pnpm-debug.log* 12 | 13 | 14 | # environment variables 15 | .env 16 | .env.production 17 | 18 | # macOS-specific files 19 | .DS_Store 20 | 21 | # Astro 22 | .astro/ 23 | 24 | # VScode 25 | .vscode/ -------------------------------------------------------------------------------- /src/components/icons/playLineIcon.astro: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/icons/telegramIcon.astro: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/content/episode/post1.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Demo Episode 3 | audioUrl: https://ia800505.us.archive.org/15/items/astropod/1%20-%20Trailer%20with%20BG%20%28enhanced%29.ogg 4 | pubDate: Sep 10 2022 5 | duration: 00:32 6 | size: 0.7 7 | cover: "" 8 | explicit: false 9 | episode: 1 10 | season: 1 11 | episodeType: full 12 | --- 13 | This is an episode demo of Astropod. Here you can add the description of your episode. -------------------------------------------------------------------------------- /src/components/icons/deezerIcon.astro: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/components/icons/heartIcon.astro: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/icons/facebookIcon.astro: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Footer.astro: -------------------------------------------------------------------------------- 1 | --- 2 | const today = new Date(); 3 | --- 4 | 5 | 13 | 14 | -------------------------------------------------------------------------------- /src/components/icons/newsletterIcon.astro: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/components/icons/youtubeIcon.astro: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/components/icons/ivooxIcon.astro: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/components/icons/pauseLineIcon.astro: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/components/icons/stopLineIcon.astro: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/components/icons/instagramIcon.astro: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/components/icons/downloadIcon.astro: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/components/icons/playIcon.astro: -------------------------------------------------------------------------------- 1 | 13 | 14 | 18 | -------------------------------------------------------------------------------- /src/components/icons/rssIcon.astro: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/components/icons/spotifyIcon.astro: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/components/icons/externalLinkIcon.astro: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/components/icons/audioIcon.astro: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 | 18 | -------------------------------------------------------------------------------- /src/components/icons/linkedinIcon.astro: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /tailwind.config.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'], 4 | theme: { 5 | extend: {}, 6 | }, 7 | plugins: [require("@tailwindcss/typography"),require("daisyui")], 8 | daisyui: { 9 | themes: true, // true: all themes | false: only light + dark | array: specific themes like this ["light", "dark", "cupcake"] 10 | darkTheme: "dark", // name of one of the included themes for dark mode 11 | logs: false, // Shows info about daisyUI version and used config in the console when building your CSS 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/components/icons/applePodcastIcon.astro: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/pages/404.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import BaseLayout from "../layouts/BaseLayout.astro"; 3 | import astropodConfig from "../../.astropod/astropod.config.json"; 4 | --- 5 | 6 | 7 |
8 | {astropodConfig.name 13 |

404

14 |

The page you're looking for couldn't be found.

15 | Home 16 |
17 |
18 | -------------------------------------------------------------------------------- /src/components/icons/githubIcon.astro: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/icons/googlePodcastIcon.astro: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/components/icons/headphoneIcon.astro: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/components/icons/twitterIcon.astro: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/icons/pauseIcon.astro: -------------------------------------------------------------------------------- 1 | 13 | 14 | 18 | 22 | -------------------------------------------------------------------------------- /.astropod/astropod.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "copyright": "", 3 | "theme": "light", 4 | "fundingText": "", 5 | "author": "Author's name", 6 | "name": "Podcast Name", 7 | "banner": "/banner.webp", 8 | "pageSize": 15, 9 | "fundingUrl": "", 10 | "owner": "", 11 | "favicon": "/favicon.svg", 12 | "explicit": false, 13 | "cover": "/cover.jpg", 14 | "language": "en", 15 | "link": "https://your-website-url.netlify.app", 16 | "email": "your.email@gmail.com", 17 | "description": "A short description of your podcast. We suggest creating a podcast description between 600-900 characters or 120-170 words in length.", 18 | "category": [ 19 | "Technology", 20 | "Business" 21 | ], 22 | "feedSize": "" 23 | } -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # github: USERNAME or github: [USERNAME, USERNAME, USERNAME, USERNAME] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: #manuelernestogr 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: ["https://manuelernestog.github.io/support-my-work/"] 13 | -------------------------------------------------------------------------------- /src/components/icons/audioMutedIcon.astro: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 | 19 | 20 | -------------------------------------------------------------------------------- /src/content/config.ts: -------------------------------------------------------------------------------- 1 | import { z, defineCollection } from "astro:content"; 2 | 3 | const episodeSchema = z.object({ 4 | title: z.string(), 5 | audioUrl: z.string(), 6 | pubDate: z.coerce.date().optional(), 7 | cover: z.string().optional(), 8 | explicit: z.boolean().optional(), 9 | episode: z.number().optional(), 10 | season: z.number().optional(), 11 | episodeType: z.string().optional(), 12 | duration: z.coerce.string(), //duration in format hh:mm:ss 13 | size: z.number(), // size in megabytes 14 | }); 15 | 16 | export type episodeSchema = z.infer; 17 | 18 | const episodeCollection = defineCollection({ schema: episodeSchema }); 19 | 20 | export const collections = { 21 | 'episode': episodeCollection, 22 | } -------------------------------------------------------------------------------- /src/components/icons/acastPodcastsIcon.astro: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/components/icons/fountainIcon.astro: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/layouts/BaseLayout.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import BaseHead from "../components/BaseHead.astro"; 3 | import Footer from "../components/Footer.astro"; 4 | import astropodConfig from "../../.astropod/astropod.config.json"; 5 | 6 | import { SITE_TITLE, SITE_DESCRIPTION } from "../config"; 7 | 8 | const { image, title = astropodConfig.name, description = astropodConfig.description } = Astro.props; 9 | --- 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 |
19 |
20 | 21 |
22 |
23 |
24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /src/layouts/EpisodeLayout.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import BaseLayout from "./BaseLayout.astro"; 3 | import dayjs from "dayjs"; 4 | import localizedFormat from "dayjs/plugin/localizedFormat"; 5 | import HeaderCard from "../components/headerCard/HeaderCard.astro"; 6 | import astropodConfig from "../../.astropod/astropod.config.json"; 7 | 8 | const { title, explicit, pubDate, cover, description } = Astro.props; 9 | dayjs.extend(localizedFormat); 10 | const displayDate = dayjs(pubDate).format("ll"); 11 | --- 12 | 13 | 14 | 15 | 16 |
17 |
18 | 19 |
20 |
21 |
22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "astropod", 3 | "description": "Astropod is a free and open-source serverless podcast template. Host your podcast for free using online third-party services.", 4 | "type": "module", 5 | "author": "Manuel Ernesto Garcia", 6 | "version": "1.0.0", 7 | "private": true, 8 | "scripts": { 9 | "dev": "astro dev", 10 | "start": "astro dev", 11 | "build": "astro build", 12 | "preview": "astro preview", 13 | "astro": "astro" 14 | }, 15 | "dependencies": { 16 | "@astrojs/image": "^0.17.2", 17 | "@astrojs/mdx": "^0.19.7", 18 | "@astrojs/rss": "^2.4.3", 19 | "@astrojs/sitemap": "^1.3.3", 20 | "@astrojs/tailwind": "^3.1.3", 21 | "astro": "^2.7.3", 22 | "astro-netlify-cms": "^0.5.3", 23 | "daisyui": "^3.1.7", 24 | "dayjs": "^1.11.9", 25 | "marked": "^7.0.2", 26 | "marked-plaintext": "^0.0.2", 27 | "sharp": "^0.32.1", 28 | "xml2js": "^0.6.2" 29 | }, 30 | "devDependencies": { 31 | "@tailwindcss/typography": "^0.5.9", 32 | "astro-robots-txt": "^0.5.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /astro.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "astro/config"; 2 | import mdx from "@astrojs/mdx"; 3 | import sitemap from "@astrojs/sitemap"; 4 | import tailwind from "@astrojs/tailwind"; 5 | import NetlifyCMS from "astro-netlify-cms"; 6 | import dcapConfig from "./decap.config.mjs"; 7 | import astropodConfig from "./.astropod/astropod.config.json"; 8 | import robotsTxt from "astro-robots-txt"; 9 | 10 | // https://astro.build/config 11 | import image from "@astrojs/image"; 12 | 13 | // https://astro.build/config 14 | export default defineConfig({ 15 | site: astropodConfig.site, 16 | integrations: [ 17 | robotsTxt({ 18 | policy: [ 19 | { 20 | userAgent: "*", 21 | allow: "/", 22 | disallow: "/admin", 23 | }, 24 | ], 25 | }), 26 | mdx(), 27 | sitemap(), 28 | tailwind(), 29 | image({ 30 | serviceEntryPoint: "@astrojs/image/sharp", 31 | cacheDir: "./.cache/image", 32 | logLevel: "debug", 33 | }), 34 | NetlifyCMS({ 35 | config: dcapConfig(), 36 | }), 37 | ], 38 | }); 39 | -------------------------------------------------------------------------------- /src/helpers/podcastMainCategories.json: -------------------------------------------------------------------------------- 1 | [ 2 | { "label": "Arts", "value": "Arts" }, 3 | { "label": "Business", "value": "Business" }, 4 | { "label": "Comedy", "value": "Comedy" }, 5 | { "label": "Education", "value": "Education" }, 6 | { "label": "Fiction", "value": "Fiction" }, 7 | { "label": "Government", "value": "Government" }, 8 | { "label": "History", "value": "History" }, 9 | { "label": "Health & Fitness", "value": "Health & Fitness" }, 10 | { "label": "Kids & Family", "value": "Kids & Family" }, 11 | { "label": "Leisure", "value": "Leisure" }, 12 | { "label": "Music", "value": "Music" }, 13 | { "label": "News", "value": "News" }, 14 | { "label": "Religion & Spirituality", "value": "Religion & Spirituality" }, 15 | { "label": "Science", "value": "Science" }, 16 | { "label": "Society & Culture", "value": "Society & Culture" }, 17 | { "label": "Sports", "value": "Sports" }, 18 | { "label": "Technology", "value": "Technology" }, 19 | { "label": "True Crime", "value": "True & Crime" }, 20 | { "label": "TV & Film", "value": "TV & Film" } 21 | ] 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Manuel Ernesto Garcia 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/components/icons/amazonMusicIcon.astro: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/components/icons/easyPodcastIcon.astro: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /src/components/pagination.astro: -------------------------------------------------------------------------------- 1 | --- 2 | const { page } = Astro.props; 3 | 4 | function getPagination(current, last, size) { 5 | let pagination = [current]; 6 | const offset = Math.floor(size / 2); 7 | const leftOffset = last - current > offset ? offset : last - current; 8 | const rightOffset = current - 1 > offset ? offset : current - 1; 9 | for (let i = 1; i < 5 - leftOffset; i++) if (current - i > 0) pagination.unshift(current - i); 10 | for (let i = 1; i < 5 - rightOffset; i++) if (current + i <= last) pagination.push(current + i); 11 | return pagination; 12 | } 13 | 14 | const pagination = getPagination(page.currentPage, page.lastPage, 5); 15 | --- 16 | 17 |
18 |
19 | { 20 | page.url.prev && ( 21 | 22 | « 23 | 24 | ) 25 | } 26 | { 27 | pagination.map((item) => ( 28 | 32 | {item} 33 | 34 | )) 35 | } 36 | 37 | { 38 | page.url.next && ( 39 | 40 | » 41 | 42 | ) 43 | } 44 |
45 |
46 | -------------------------------------------------------------------------------- /src/helpers/themes.json: -------------------------------------------------------------------------------- 1 | [ 2 | { "label": "Light", "value": "light" }, 3 | { "label": "Dark", "value": "dark" }, 4 | { "label": "Cupcake", "value": "cupcake" }, 5 | { "label": "Bumblebee", "value": "bumblebee" }, 6 | { "label": "Emerald", "value": "emerald" }, 7 | { "label": "Corporate", "value": "corporate" }, 8 | { "label": "Synthwave", "value": "synthwave" }, 9 | { "label": "Retro", "value": "retro" }, 10 | { "label": "Cyberpunk", "value": "cyberpunk" }, 11 | { "label": "Valentine", "value": "valentine" }, 12 | { "label": "Halloween", "value": "halloween" }, 13 | { "label": "Garden", "value": "garden" }, 14 | { "label": "Forest", "value": "forest" }, 15 | { "label": "Aqua", "value": "aqua" }, 16 | { "label": "Lofi", "value": "lofi" }, 17 | { "label": "Pastel", "value": "pastel" }, 18 | { "label": "Fantasy", "value": "fantasy" }, 19 | { "label": "Wireframe", "value": "wireframe" }, 20 | { "label": "Black", "value": "black" }, 21 | { "label": "Luxury", "value": "luxury" }, 22 | { "label": "Dracula", "value": "dracula" }, 23 | { "label": "CMYK", "value": "cmyk" }, 24 | { "label": "Autumn", "value": "autumn" }, 25 | { "label": "Business", "value": "business" }, 26 | { "label": "Acid", "value": "acid" }, 27 | { "label": "Lemonade", "value": "lemonade" }, 28 | { "label": "Night", "value": "night" }, 29 | { "label": "Coffee", "value": "coffee" }, 30 | { "label": "Winter", "value": "winter" } 31 | ] -------------------------------------------------------------------------------- /src/components/BaseHead.astro: -------------------------------------------------------------------------------- 1 | --- 2 | // Import the global.css file here so that it is included on 3 | // all pages through the use of the component. 4 | export interface Props { 5 | title: string; 6 | description: string; 7 | image?: string; 8 | } 9 | 10 | import astropodConfig from "../../.astropod/astropod.config.json"; 11 | const { title, description, image } = Astro.props; 12 | 13 | const display_image = image ? image : astropodConfig.cover; 14 | --- 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | {title} 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/pages/[...page].astro: -------------------------------------------------------------------------------- 1 | --- 2 | import BaseLayout from "../layouts/BaseLayout.astro"; 3 | import HorizontalCard from "../components/HorizontalCard.astro"; 4 | import { getCollection } from "astro:content"; 5 | import astropodConfig from "../../.astropod/astropod.config.json"; 6 | import HeaderCard from "../components/headerCard/HeaderCard.astro"; 7 | import AudioPlayer from "../components/audioPlayer.astro"; 8 | import PlainTextRenderer from "marked-plaintext"; 9 | import { marked } from "marked"; 10 | import Pagination from "../components/pagination.astro"; 11 | 12 | const renderer = new PlainTextRenderer(); 13 | 14 | export async function getStaticPaths({ paginate }) { 15 | const episodes = await getCollection("episode"); 16 | episodes.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf()); 17 | return paginate(episodes, { pageSize: astropodConfig.pageSize }); 18 | } 19 | const { page } = Astro.props; 20 | --- 21 | 22 | 23 | 24 | 25 |
26 | { 27 | page.data.map((episode, index) => ( 28 | 38 | )) 39 | } 40 |
41 | {(page.url.next || page.url.prev) && } 42 |
43 | 44 | 45 | -------------------------------------------------------------------------------- /src/components/headerCard/followUsMenu.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import social from "../../../.astropod/social.config.json"; 3 | import TwitterIcon from "../icons/twitterIcon.astro"; 4 | import YoutubeIcon from "../icons/youtubeIcon.astro"; 5 | import TelegramIcon from "../icons/telegramIcon.astro"; 6 | import FacebookIcon from "../icons/facebookIcon.astro"; 7 | import InstagramIcon from "../icons/instagramIcon.astro"; 8 | import NewsletterIcon from "../icons/newsletterIcon.astro"; 9 | import LinkedinIcon from "../icons/linkedinIcon.astro"; 10 | import GithubIcon from "../icons/githubIcon.astro"; 11 | 12 | 13 | import ExternalLinkIcon from "../icons/externalLinkIcon.astro"; 14 | 15 | let hasSocial = false 16 | 17 | for (const key in social) { 18 | if (social[key]){ 19 | hasSocial = true; 20 | break; 21 | } 22 | } 23 | --- 24 | {hasSocial && ( 25 | 41 | )} 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/pages/episode/[slug].astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { CollectionEntry, getCollection } from "astro:content"; 3 | import { episodeSchema } from "../../content/config"; 4 | import EpisodeLayout from "../../layouts/EpisodeLayout.astro"; 5 | import AudioPlayer from "../../components/audioPlayer.astro"; 6 | import PlainTextRenderer from "marked-plaintext"; 7 | import { marked } from "marked"; 8 | import astropodConfig from "../../../.astropod/astropod.config.json"; 9 | 10 | export async function getStaticPaths() { 11 | const postEntries = await getCollection("episode"); 12 | return postEntries.map((entry) => ({ 13 | params: { slug: entry.slug }, 14 | props: { entry }, 15 | })); 16 | } 17 | 18 | interface Props { 19 | entry: CollectionEntry<"episode">; 20 | } 21 | 22 | const { entry } = Astro.props; 23 | const episode: episodeSchema = entry.data; 24 | const { Content } = await entry.render(); 25 | 26 | const renderer = new PlainTextRenderer(); 27 | const description = marked.parse(entry.body.slice(0, 500), { renderer: renderer }).slice(0, 200); 28 | --- 29 | 30 | 38 | 39 | 40 | 41 | 42 | 43 | 59 | -------------------------------------------------------------------------------- /src/components/headerCard/listenOnMenu.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import listenOn from "../../../.astropod/listenOn.config.json"; 3 | import GooglePodcastIcon from "../icons/googlePodcastIcon.astro"; 4 | import SpotifyIcon from "../icons/spotifyIcon.astro"; 5 | import ApplePodcastIcon from "../icons/applePodcastIcon.astro"; 6 | import HeadphoneIcon from "../icons/headphoneIcon.astro"; 7 | import PocketcastsIcon from "../icons/pocketcastsIcon.astro"; 8 | import IvooxIcon from "../icons/ivooxIcon.astro"; 9 | import AmazonMusicIcon from "../icons/amazonMusicIcon.astro"; 10 | import FountainIcon from "../icons/fountainIcon.astro"; 11 | import DeezerIcon from "../icons/deezerIcon.astro"; 12 | import EasyPodcastIcon from "../icons/easyPodcastIcon.astro"; 13 | import AcastPodcastsIcon from "../icons/acastPodcastsIcon.astro"; 14 | 15 | let hasListenOn = false 16 | 17 | for (const key in listenOn) { 18 | if (listenOn[key]){ 19 | hasListenOn = true; 20 | break; 21 | } 22 | } 23 | --- 24 | 25 | {hasListenOn && ( 26 | 44 | )} 45 | -------------------------------------------------------------------------------- /src/components/HorizontalCard.astro: -------------------------------------------------------------------------------- 1 | --- 2 | const { title, cover, desc, url, audioUrl, id, pubDate, duration } = Astro.props; 3 | 4 | import astropodConfig from "../../.astropod/astropod.config.json"; 5 | import dayjs from "dayjs"; 6 | import localizedFormat from "dayjs/plugin/localizedFormat"; 7 | dayjs.extend(localizedFormat); 8 | import PlayIcon from "./icons/playIcon.astro"; 9 | import PauseIcon from "./icons/pauseIcon.astro"; 10 | 11 | let cover_url = cover ? cover : astropodConfig.cover; 12 | const displayDate = dayjs(pubDate).format("ll"); 13 | 14 | function formatTime(secs) { 15 | if (isNaN(secs) || secs < 0) { 16 | const segmentedTime = secs.split(":"); 17 | return segmentedTime[0] == "00" || segmentedTime[0] == "0" ? secs : null; 18 | } 19 | 20 | const hours = Math.floor(secs / 3600); 21 | const minutes = Math.floor((secs % 3600) / 60); 22 | const seconds = Math.floor(secs % 60); 23 | const formattedMinutes = minutes < 10 ? `0${minutes}` : minutes; 24 | const formattedseconds = seconds < 10 ? `0${seconds}` : seconds; 25 | 26 | return hours > 0 ? `${hours}:${formattedMinutes}:${formattedseconds}` : `${minutes}:${formattedseconds}`; 27 | } 28 | 29 | const displayDuration = formatTime(duration); 30 | --- 31 | 32 | 61 | 62 | 67 | -------------------------------------------------------------------------------- /src/helpers/languagesList.json: -------------------------------------------------------------------------------- 1 | [ 2 | { "label": "English", "value": "en" }, 3 | { "label": "Mandarin Chinese", "value": "zh" }, 4 | { "label": "Hindi", "value": "hi" }, 5 | { "label": "Spanish", "value": "es" }, 6 | { "label": "French", "value": "fr" }, 7 | { "label": "Standard Arabic", "value": "ar" }, 8 | { "label": "Bengali", "value": "bn" }, 9 | { "label": "Russian", "value": "ru" }, 10 | { "label": "Portuguese", "value": "pt" }, 11 | { "label": "Indonesian", "value": "id" }, 12 | { "label": "Urdu", "value": "ur" }, 13 | { "label": "German", "value": "de" }, 14 | { "label": "Japanese", "value": "ja" }, 15 | { "label": "Swahili", "value": "sw" }, 16 | { "label": "Marathi", "value": "mr" }, 17 | { "label": "Telugu", "value": "te" }, 18 | { "label": "Turkish", "value": "tr" }, 19 | { "label": "Tamil", "value": "ta" }, 20 | { "label": "Vietnamese", "value": "vi" }, 21 | { "label": "Korean", "value": "ko" }, 22 | { "label": "Italian", "value": "it" }, 23 | { "label": "Yoruba", "value": "yo" }, 24 | { "label": "Igbo", "value": "ig" }, 25 | { "label": "Odia", "value": "or" }, 26 | { "label": "Thai", "value": "th" }, 27 | { "label": "Ukrainian", "value": "uk" }, 28 | { "label": "Dutch", "value": "nl" }, 29 | { "label": "Sindhi", "value": "sd" }, 30 | { "label": "Amharic", "value": "am" }, 31 | { "label": "Arabic (Egyptian Spoken)", "value": "arz" }, 32 | { "label": "Tagalog", "value": "tl" }, 33 | { "label": "Nepali", "value": "ne" }, 34 | { "label": "Sundanese", "value": "su" }, 35 | { "label": "Romanian", "value": "ro" }, 36 | { "label": "Bhojpuri", "value": "bho" }, 37 | { "label": "Azerbaijani", "value": "az" }, 38 | { "label": "Farsi", "value": "fa" }, 39 | { "label": "Maithili", "value": "mai" }, 40 | { "label": "Hausa", "value": "ha" }, 41 | { "label": "Burmese", "value": "my" }, 42 | { "label": "Serbo-Croatian", "value": "sh" }, 43 | { "label": "Uzbek", "value": "uz" }, 44 | { "label": "Pashto", "value": "ps" }, 45 | { "label": "Kannada", "value": "kn" }, 46 | { "label": "Xhosa", "value": "xh" }, 47 | { "label": "Oromo", "value": "om" }, 48 | { "label": "Malayalam", "value": "ml" }, 49 | { "label": "Haitian Creole", "value": "ht" }, 50 | { "label": "Sinhalese", "value": "si" }, 51 | { "label": "Zulu", "value": "zu" }, 52 | { "label": "Cebuano", "value": "ceb" }, 53 | { "label": "Amoy Min Nan Chinese", "value": "nan" }, 54 | { "label": "Bavarian", "value": "bar" }, 55 | { "label": "Bhojpuri", "value": "bho" }, 56 | { "label": "Burmese", "value": "my" } 57 | ] 58 | -------------------------------------------------------------------------------- /src/components/headerCard/HeaderCard.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import astropodConfig from "../../../.astropod/astropod.config.json"; 3 | const { title, subtitle, explicit, cover } = Astro.props; 4 | 5 | import ListenOnMenu from "./listenOnMenu.astro"; 6 | import FollowUsMenu from "./followUsMenu.astro"; 7 | import RssIcon from "../icons/rssIcon.astro"; 8 | import HeartIcon from "../icons/heartIcon.astro"; 9 | 10 | const explicitLabel = explicit === undefined ? astropodConfig.explicit : explicit; 11 | let cover_url = cover ? cover : astropodConfig.cover; 12 | --- 13 | 14 |
15 |
16 |
17 | {astropodConfig.name 18 |
21 |
22 |

{subtitle}

23 |

{title}

24 |
25 |
26 | { 27 | explicitLabel && ( 28 | 29 | EXPLICIT 30 | 31 | ) 32 | } 33 |
34 | {astropodConfig.name 39 |
40 | 41 |
42 | Home 43 | About 44 | 45 |
46 | 47 | 48 | 49 | 50 | 51 | { 52 | astropodConfig.fundingUrl && ( 53 | 58 | 59 | 60 | ) 61 | } 62 | 63 | 64 | 65 | 66 |
67 |
68 | -------------------------------------------------------------------------------- /public/favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/pages/rss.xml.js: -------------------------------------------------------------------------------- 1 | import xml2js from "xml2js"; 2 | import dayjs from "dayjs"; 3 | import astropodConfig from "../../.astropod/astropod.config.json"; 4 | import { getCollection } from "astro:content"; 5 | let episode = await getCollection("episode"); 6 | episode.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf()); 7 | if (astropodConfig.feedSize) episode = episode.slice(0, astropodConfig.feedSize); 8 | 9 | import { marked } from "marked"; 10 | 11 | const lastBuildDate = dayjs().format("ddd, DD MMM YYYY hh:mm:ss ZZ"); 12 | const cover = isFullUrl(astropodConfig.cover) ? astropodConfig.cover : astropodConfig.link + astropodConfig.cover; 13 | 14 | export async function get(context) { 15 | let podcast = { 16 | rss: { 17 | $: { 18 | version: "2.0", 19 | "xmlns:itunes": "http://www.itunes.com/dtds/podcast-1.0.dtd", 20 | "xmlns:podcast": "https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md", 21 | "xmlns:atom": "http://www.w3.org/2005/Atom", 22 | "xmlns:content": "http://purl.org/rss/1.0/modules/content/", 23 | }, 24 | channel: [ 25 | { 26 | title: astropodConfig.name, 27 | description: astropodConfig.description, 28 | link: astropodConfig.link, 29 | copyright: astropodConfig.copyright, 30 | author: astropodConfig.author, 31 | generator: ["Astropod"], 32 | lastBuildDate: lastBuildDate, 33 | language: astropodConfig.language, 34 | "itunes:author": astropodConfig.author, 35 | "itunes:image": { $: { href: cover } }, 36 | "itunes:summary": astropodConfig.description, 37 | "itunes:type": "episodic", 38 | "itunes:explicit": astropodConfig.explicit, 39 | "itunes:owner": { 40 | "itunes:name": astropodConfig.owner, 41 | "itunes:email": astropodConfig.email, 42 | }, 43 | image: { 44 | link: astropodConfig.link, 45 | title: astropodConfig.name, 46 | url: cover, 47 | }, 48 | "atom:link": [ 49 | { 50 | $: { 51 | href: `${astropodConfig.link}/rss.xml`, 52 | rel: "self", 53 | type: "application/rss+xml", 54 | }, 55 | }, 56 | { 57 | $: { 58 | href: `https://pubsubhubbub.appspot.com/`, 59 | rel: "hub", 60 | type: "application/rss+xml", 61 | }, 62 | }, 63 | ], 64 | }, 65 | ], 66 | }, 67 | }; 68 | 69 | podcast.rss.channel[0].category = astropodConfig.category.map((category) => category); 70 | podcast.rss.channel[0]["itunes:category"] = astropodConfig.category.map((category) => ({ 71 | $: { 72 | text: category, 73 | }, 74 | })); 75 | 76 | // podcast.rss.channel[0]["itunes:category"] = { 77 | // $: { 78 | // text: astropodConfig.category, 79 | // }, 80 | // }; 81 | 82 | if (astropodConfig.fundingUrl) { 83 | const fundingUrl = isFullUrl(astropodConfig.fundingUrl) 84 | ? astropodConfig.fundingUrl 85 | : astropodConfig.link + astropodConfig.fundingUrl; 86 | podcast.rss.channel[0]["podcast:funding"] = { 87 | $: { url: fundingUrl }, 88 | _: astropodConfig.fundingText, 89 | }; 90 | } 91 | 92 | const items = episode.map((episode) => { 93 | let item = { 94 | title: episode.data.title, 95 | description: marked.parse(episode.body), 96 | pubDate: dayjs(episode.data.pubDate).format("ddd, DD MMM YYYY hh:mm:ss ZZ"), 97 | link: `${astropodConfig.link}/episode/${episode.slug}/`, 98 | guid: `${astropodConfig.link}/episode/${episode.slug}/`, 99 | "itunes:episode": episode.data.episode, 100 | "itunes:season": episode.data.season, 101 | "itunes:episodeType": episode.data.episodeType, 102 | "itunes:explicit": episode.data.explicit === undefined ? astropodConfig.explicit : episode.data.explicit, 103 | enclosure: { 104 | $: { 105 | url: isFullUrl(episode.data.audioUrl) ? episode.data.audioUrl : astropodConfig.link + episode.data.audioUrl, 106 | length: episode.data.size && episode.data.size * 1000000, 107 | type: "audio/mpeg", 108 | }, 109 | }, 110 | "itunes:duration": episode.data.duration, 111 | }; 112 | const cover_url = episode.data.cover ? episode.data.cover : astropodConfig.cover; 113 | item["itunes:image"] = { 114 | $: { href: isFullUrl(cover_url) ? cover_url : astropodConfig.link + cover_url }, 115 | }; 116 | return item; 117 | }); 118 | 119 | podcast.rss.channel[0].item = items; 120 | 121 | let builder = new xml2js.Builder({cdata: true}); 122 | let xml = builder.buildObject(podcast); 123 | 124 | return { 125 | body: xml, 126 | }; 127 | } 128 | 129 | function isFullUrl(urlString) { 130 | try { 131 | return Boolean(new URL(urlString)); 132 | } catch (e) { 133 | return false; 134 | } 135 | } -------------------------------------------------------------------------------- /src/components/audioPlayer.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import DownloadIcon from "./icons/downloadIcon.astro"; 3 | import PlayIcon from "./icons/playIcon.astro"; 4 | import PauseIcon from "./icons/pauseIcon.astro"; 5 | import AudioIcon from "./icons/audioIcon.astro"; 6 | import AudioMutedIcon from "./icons/audioMutedIcon.astro"; 7 | --- 8 | 9 | 66 | 67 | 165 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Astropod | Free and Open Source Serverless Podcast 2 | 3 | ![Atropod](https://github.com/manuelernestog/manuelernestog/assets/53962116/3106bf65-37f9-427f-9b50-72d7ae22752f) 4 | 5 | Astropod is a free and open-source self-hosting serverless podcast solution. It supports diverse deployment services and audio hosting options and includes a customizable website deployable in a few clicks, with a personalized domain and a user-friendly CMS for effortless podcast management. 6 | 7 | --- 8 | 9 | ## Demo and Tutorial 10 | 11 | View a live demo and a tutorial step by step of how deploy and configure your podcast with Astropod. 12 | 13 | 👉️ 14 | 15 | --- 16 | 17 | ## Quick deploy 18 | 19 | [![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)][deploy] 20 | 21 | - Click the Deploy button to copy this project to your own GitHub or GitLab account and deploy your podcast with Netlify in minutes. ✨ 22 | 23 | - Once you’ve got the project set up, you do need to 24 | [activate Netlify Identity in the Netlify UI][identity], the 25 | [“Git Gateway”][gateway] to allow e-mail/password authentication and you are ready to go. 26 | 27 | --- 28 | 29 | ## Deployment Services 30 | 31 | Astropod is build in the top of [Astro](https://astro.build/) framework and is ready to be deployed in the deployments services like GitHub Pages, Netlify, Vercel, Cloudflare, AWS and much more. 32 | 33 | The configuration for the deployment varies depending on the platform where you are going to do it. See the [official Astro information](https://docs.astro.build/en/guides/deploy/) to deploy your website. 34 | 35 | ## File storage 36 | 37 | Astropod is designed to work with any file storage solution. 38 | 39 | You can host your podcast files in your own code base inside the folder `public/audio` and this will automatically work with [git LFS](https://git-lfs.com/) but free plans in GitHub and GitLab for LFS are limited so take that in mind. 40 | 41 | You can also host your podcast files in any other cloud storage solution like DropBox, Google Drive, Cloudinary or on your own server. Once you upload your audio file you just need to copy the public URL of the audio file into your episode `AudioUrl` field. 42 | 43 | For free unlimited cloud file storage we recommend use [Internet Archive](https://archive.org/). 44 | 45 | ## Content Management System 46 | 47 | Astropod is designed in such a way that you can use it directly from the source code by editing the configuration files found in the `/.astropod` folder and modifying the episode markdown files contained in the folder `/src/content/episode`. 48 | 49 | In order to manage podcast in a simpler way, astropod is configured by default with the CMS Serverless [Decap](https://decapcms.org/) which allows collaborative work, authentication with email or other providers and simple management. 50 | 51 | For the easiest use of Decap we recommend deploying Astropod on Netlify since the authentication system is integrated into the platform and is configured natively. If you would like to facilitate your own OAuth authentication rather than use Netlify's service or a client side flow like implicit or PKCE, you can use one of this [community-maintained projects](https://decapcms.org/docs/external-oauth-clients/). 52 | 53 | The fact that Astropod comes configured by default with Decap does not mean that it is the only CMS with which it can work, if you prefer to use another CMS like Sanity, Wordpress, Strapi, Tina, or any other you can follow [Astro's integration guides](https://docs.astro.build/en/guides/cms/) to your integrate your favorite CMS. 54 | 55 | ## Tech Stack 56 | 57 | - [Astro](https://astro.build) 58 | - [Tailwind](https://tailwindcss.com/) 59 | - [DaisyUI](https://daisyui.com/) 60 | 61 | ## Requirements 62 | 63 | - Node 16.16.0 or higher 64 | 65 | ## Commands 66 | 67 | All commands are run from the root of the project, from a terminal: 68 | 69 | | Command | Action | 70 | | :----------------- | :------------------------------------------------- | 71 | | `pnpm install` | Installs dependencies | 72 | | `pnpm run dev` | Starts local dev & Netlify CMS proxy servers | 73 | | `pnpm run build` | Build your production site to `./dist/` | 74 | | `pnpm run preview` | Serve `./dist/` & run the Netlify CMS proxy server | 75 | 76 | > These commands are using [`pnpm`][pnpm], but you can choose to use `npm` or `yarn` instead if you prefer. 77 | 78 | ## Project Structure 79 | 80 | ```txt 81 | ├── src/ 82 | │ ├── components/ // UI components 83 | │ ├── content/ 84 | │ │ ├── episode/ // Podcast episodes folder 85 | │ ├── layouts/ // UI Layouts 86 | │ ├── helpers/ // App helpers like static data or functions 87 | │ └── pages/ 88 | │ │ ├── rss.xml.js/ // Feed RSS generation file 89 | ├── public/ // Public folder dor media files 90 | │ ├── audio/ // Git LFS folder for audio storage 91 | ├── .astropod/ // Astropod config files folder 92 | ├── astro.config.mjs // Astro config file 93 | ├── decap.config.mjs // Decap CMS config file 94 | ``` 95 | 96 | ## Contributing 97 | 98 | Suggestions and pull requests are welcomed! Feel free to open a discussion or an issue for a new feature request or bug. 99 | 100 | One of the best ways of contribute is to grab a [bug report or feature suggestion](https://github.com/manuelernestog/astropod/issues) that has been marked `accepted` and dig in. 101 | 102 | Please be wary of working on issues _not_ marked as `accepted`. Just because someone has created an issue doesn't mean we'll accept a pull request for it. 103 | 104 | ## License 105 | 106 | Astropod is licensed under the MIT license — see the [LICENSE](https://github.com/manuelernestog/astropod/blob/main/LICENSE) file for details. 107 | 108 | ## Contributors 109 | 110 | [![Manuel Ernestog](https://contrib.rocks/image?repo=manuelernestog/astropod)](https://github.com/manuelernestog/astropod/graphs/contributors) 111 | 112 | Made with [contrib.rocks](https://contrib.rocks). 113 | 114 | [deploy]: https://app.netlify.com/start/deploy?repository=https://github.com/manuelernestog/astropod 115 | [identity]: https://docs.netlify.com/visitor-access/identity/ 116 | [gateway]: https://docs.netlify.com/visitor-access/git-gateway/ 117 | [pnpm]: https://pnpm.io/ 118 | -------------------------------------------------------------------------------- /decap.config.mjs: -------------------------------------------------------------------------------- 1 | import astropodConfig from "./.astropod/astropod.config.json"; 2 | import languageList from "./src/helpers/languagesList.json"; 3 | import podcastMainCategories from "./src/helpers/podcastMainCategories.json"; 4 | import themes from "./src/helpers/themes.json"; 5 | 6 | export default function dcapconfig() { 7 | const config = { 8 | logo_url: "/cmsBanner.svg", 9 | backend: { 10 | name: "git-gateway", 11 | branch: "main", 12 | }, 13 | collections: [ 14 | { 15 | name: "episodes", 16 | label: "Episodes", 17 | label_singular: "Episode", 18 | folder: "src/content/episode", 19 | sortable_fields: ["title", "pubDate", "episode", "season"], 20 | create: true, 21 | delete: true, 22 | fields: [ 23 | { name: "title", widget: "string", label: "Episode Title" }, 24 | { name: "audioUrl", widget: "string", label: "Audio URL" }, 25 | { name: "pubDate", widget: "date", label: "Publish Date", format: "DD MMM YYYY" }, 26 | { name: "body", widget: "markdown", label: "Episode Body", required: false }, 27 | { 28 | name: "duration", 29 | widget: "string", 30 | label: "Episode Duration", 31 | pattern: [ 32 | "^(?:[01]?[0-9]|2[0-3]):[0-5]?[0-9]:[0-5]?[0-9]$|^[0-5]?[0-9]:[0-5]?[0-9]$", 33 | "Must have format hh:mm:ss or mm:ss", 34 | ], 35 | }, 36 | { name: "size", widget: "number", label: "Episode Size (MB)", value_type: "float" }, 37 | { name: "cover", widget: "image", label: "Custom Cover URL", required: false }, 38 | { name: "explicit", widget: "boolean", label: "Explicit", required: false, default: astropodConfig.explicit }, 39 | { name: "episode", widget: "number", label: "Episode", required: false }, 40 | { name: "season", widget: "number", label: "Season", required: false }, 41 | { 42 | name: "episodeType", 43 | widget: "select", 44 | label: "Episode Type", 45 | default: "full", 46 | options: [ 47 | { label: "Full", value: "full" }, 48 | { label: "Trailer", value: "trailer" }, 49 | { label: "Bonus", value: "bonus" }, 50 | ], 51 | }, 52 | ], 53 | }, 54 | { 55 | name: "settings", 56 | label: "Settings", 57 | files: [ 58 | { 59 | name: "general", 60 | label: "General", 61 | file: ".astropod/astropod.config.json", 62 | fields: [ 63 | { name: "name", widget: "string", label: "Name" }, 64 | { name: "description", widget: "text", label: "Description" }, 65 | { name: "link", widget: "string", label: "Link" }, 66 | { name: "cover", widget: "image", label: "Cover | Recomended Size 1400x1400" }, 67 | { name: "banner", widget: "image", label: "Banner | Recomended Size 960x320" }, 68 | { name: "favicon", widget: "image", label: "Favicon" }, 69 | 70 | { name: "author", widget: "string", label: "Author" }, 71 | { 72 | name: "email", 73 | widget: "string", 74 | label: "email", 75 | pattern: ["^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}$", "Please enter a valid email address"], 76 | }, 77 | { name: "owner", widget: "string", label: "Owner", required: false }, 78 | { name: "copyright", widget: "string", label: "Copyright", required: false }, 79 | { 80 | name: "language", 81 | widget: "select", 82 | label: "Language", 83 | default: "en", 84 | options: languageList, 85 | }, 86 | { 87 | name: "category", 88 | widget: "select", 89 | label: "Category", 90 | multiple: true, 91 | options: podcastMainCategories, 92 | max: 3, 93 | }, 94 | { name: "explicit", widget: "boolean", label: "Explicit", default: false }, 95 | { 96 | name: "theme", 97 | widget: "select", 98 | label: "Web Site Theme", 99 | options: themes, 100 | }, 101 | { name: "pageSize", widget: "number", label: "Episodes per page" }, 102 | { name: "feedSize", widget: "number", label: "Feed Size (Empty for include all episodes)", required: false }, 103 | { name: "fundingUrl", widget: "string", label: "Funding Url", required: false }, 104 | { name: "fundingText", widget: "string", label: "Funding Text", required: false }, 105 | ], 106 | }, 107 | { 108 | name: "listenOn", 109 | label: "Listen On", 110 | file: ".astropod/listenOn.config.json", 111 | fields: [ 112 | { name: "acastPodcasts", widget: "string", label: "Acast Podcasts", required: false }, 113 | { name: "spotify", widget: "string", label: "Spotify", required: false }, 114 | { name: "googlePodcast", widget: "string", label: "Google Podcast", required: false }, 115 | { name: "applePodcast", widget: "string", label: "Apple Podcast", required: false }, 116 | { name: "easyPodcast", widget: "string", label: "Easy Podcast", required: false }, 117 | { name: "amazonMusic", widget: "string", label: "Amazon Music", required: false }, 118 | { name: "fountain", widget: "string", label: "Fountain", required: false }, 119 | { name: "ivoox", widget: "string", label: "iVoox", required: false }, 120 | { name: "pocketcasts", widget: "string", label: "Pocket Casts", required: false }, 121 | { name: "deezer", widget: "string", label: "Deezer", required: false }, 122 | ], 123 | }, 124 | { 125 | name: "follow", 126 | label: "Social Links", 127 | file: ".astropod/social.config.json", 128 | fields: [ 129 | { name: "facebook", widget: "string", label: "Facebook", required: false }, 130 | { name: "twitter", widget: "string", label: "Twitter", required: false }, 131 | { name: "youtube", widget: "string", label: "YouTube", required: false }, 132 | { name: "instagram", widget: "string", label: "Instagram", required: false }, 133 | { name: "github", widget: "string", label: "Github", required: false }, 134 | { name: "telegram", widget: "string", label: "Telegram", required: false }, 135 | { name: "linkedin", widget: "string", label: "LinkedIn", required: false }, 136 | { name: "newsletter", widget: "string", label: "Newsletter", required: false }, 137 | ], 138 | }, 139 | ], 140 | }, 141 | ], 142 | disableIdentityWidgetInjection: true, 143 | }; 144 | return config; 145 | } 146 | -------------------------------------------------------------------------------- /public/cmsBanner.svg: -------------------------------------------------------------------------------- 1 | --------------------------------------------------------------------------------