Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: CI

on:
pull_request:
branches: [main, prod]
push:
branches: [main]

jobs:
build:
name: Build & type-check
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm

- name: Install dependencies
run: npm ci

# Runs `astro check && astro build` (see package.json "build" script).
- name: Build
run: npm run build
127 changes: 88 additions & 39 deletions astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,14 @@ export default defineConfig({
logo: {
src: "./src/assets/pythonlogo.png",
},
// Show "Last updated" on content pages, derived from git history.
lastUpdated: true,
components: {
Footer: "./src/components/Footer.astro",
// Injects per-page JSON-LD structured data.
Head: "./src/components/Head.astro",
// Adds sidebar search + collapse/expand-all with persisted state.
Sidebar: "./src/components/Sidebar.astro",
},
favicon: "./src/assets/favicon.ico",
social: {
Expand Down Expand Up @@ -181,13 +187,17 @@ export default defineConfig({
src: "/scripts/main.js",
},
},
{
tag: "meta",
attrs: {
name: "google-adsense-account",
content: import.meta.env.VITE_GOOGLE_ADSENSE,
},
},
...(import.meta.env.VITE_GOOGLE_ADSENSE
? [
{
tag: "meta",
attrs: {
name: "google-adsense-account",
content: import.meta.env.VITE_GOOGLE_ADSENSE,
},
},
]
: []),
{
tag: "meta",
attrs: {
Expand Down Expand Up @@ -263,13 +273,17 @@ export default defineConfig({
"Python, Python Projects, Python Central Hub, Python Central Hub Projects, Python Central Hub Tutorials, Python Central Hub Guides, Python Central Hub Reference, Python Tutorial, Python tutorials, Python programming, Learn Python, Python for beginners, Python code examples, Python development, Python projects,Python programming language, Python tips and tricks,Python resources,Python learning platform,Python coding lessons,Python programming for beginners,Python programming exercises,Python coding practice,Python syntax,Python libraries,Python community,Python best practices,Python coding challenges",
},
},
{
tag: "meta",
attrs: {
name: "monetag",
content: `${import.meta.env.VITE_MONETAG}`
}
},
...(import.meta.env.VITE_MONETAG
? [
{
tag: "meta",
attrs: {
name: "monetag",
content: `${import.meta.env.VITE_MONETAG}`,
},
},
]
: []),
{
tag: "meta",
attrs: {
Expand Down Expand Up @@ -299,18 +313,12 @@ export default defineConfig({
},
},
{
tag: "meta",
attrs: {
name: "theme-color",
content: "#e7c384",
},
},
{
tag: "script",
tag: "link",
attrs: {
async: true,
src: `https://www.googletagmanager.com/gtag/js?id=${import.meta.env.VITE_GOOGLE_ANALYTICS
}`,
rel: "alternate",
type: "application/rss+xml",
title: "Python Central Hub RSS Feed",
href: "/rss.xml",
},
},
// <script src="https://quge5.com/88/tag.min.js" data-zone="217965" async data-cfasync="false"></script>
Expand All @@ -324,12 +332,30 @@ export default defineConfig({
},
},
{
tag: "script",
tag: "meta",
attrs: {
async: true,
src: "/scripts/gtag.js",
name: "theme-color",
content: "#e7c384",
},
},
...(import.meta.env.VITE_GOOGLE_ANALYTICS
? [
{
tag: "script",
attrs: {
async: true,
src: `https://www.googletagmanager.com/gtag/js?id=${import.meta.env.VITE_GOOGLE_ANALYTICS}`,
},
},
{
tag: "script",
attrs: {
async: true,
src: "/scripts/gtag.js",
},
},
]
: []),
{
tag: "script",
attrs: {
Expand All @@ -338,16 +364,19 @@ export default defineConfig({
src: "/scripts/mermaid.js",
},
},
{
tag: "script",
attrs: {
async: true,
src: `https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=${import.meta.env.VITE_GOOGLE_ADSENSE
}`,
crossorigin: "anonymous",
},
},

...(import.meta.env.VITE_GOOGLE_ADSENSE
? [
{
tag: "script",
attrs: {
async: true,
src: `https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=${import.meta.env.VITE_GOOGLE_ADSENSE}`,
crossorigin: "anonymous",
},
},
]
: []),

{
tag: "link",
attrs: {
Expand Down Expand Up @@ -381,7 +410,27 @@ export default defineConfig({
tailwind({
applyBaseStyles: false,
}),
sitemap(),
sitemap({
// Give index/guides/tutorials higher priority than deep pages and
// stamp a build-time lastmod so crawlers see fresh dates.
serialize(item) {
const url = item.url;
if (url === `${site}/`) {
item.priority = 1.0;
} else if (url.includes("/guides/")) {
item.priority = 0.9;
} else if (url.includes("/tutorials/")) {
item.priority = 0.8;
} else if (url.includes("/projects/")) {
item.priority = 0.7;
} else {
item.priority = 0.6;
}
item.changefreq = "weekly";
item.lastmod = new Date().toISOString();
return item;
},
}),
robotsTxt(),
markdownIntegration(),
],
Expand Down
20 changes: 20 additions & 0 deletions firebase/firestore.rules.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
rules_version = '2';

// EXAMPLE Firestore rules. The public web SDK config in Contact.astro /
// Feedback.astro is client-visible, so without rules anyone can write freely.
//
// Recommended: deny direct client writes and route submissions through the
// `submitForm` Cloud Function (see functions/index.example.js), which validates
// the reCAPTCHA token server-side. The function uses the Admin SDK and bypasses
// these rules.
service cloud.firestore {
match /databases/{database}/documents {
// Block all direct client reads/writes to form collections.
match /contact/{doc} {
allow read, write: if false;
}
match /feedback/{doc} {
allow read, write: if false;
}
}
}
56 changes: 56 additions & 0 deletions firebase/functions/index.example.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/**
* Reference Cloud Function for validating reCAPTCHA v3 tokens before allowing
* a write to Firestore. This is an EXAMPLE — deploy it under your own Firebase
* Functions project and wire the client forms to POST here instead of writing
* to Firestore directly, OR call it from a Firestore `onCreate` trigger.
*
* Setup:
* 1. firebase init functions
* 2. Copy this file to functions/index.js (adjust as needed)
* 3. Set the secret: firebase functions:config:set recaptcha.secret="YOUR_SECRET"
* 4. firebase deploy --only functions
*
* The client forms (Contact.astro / Feedback.astro) attach a `recaptchaToken`
* field. Validate it here; reject low scores / failed verifications.
*/

const functions = require("firebase-functions");
const admin = require("firebase-admin");

admin.initializeApp();

const MIN_SCORE = 0.5;

async function verifyToken(token) {
const secret = functions.config().recaptcha.secret;
const res = await fetch("https://www.google.com/recaptcha/api/siteverify", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: `secret=${encodeURIComponent(secret)}&response=${encodeURIComponent(token)}`,
});
const data = await res.json();
return data.success === true && (data.score ?? 0) >= MIN_SCORE;
}

// HTTPS callable: client posts { collection, payload, recaptchaToken }.
exports.submitForm = functions.https.onCall(async (data) => {
const { collection, payload, recaptchaToken } = data || {};

if (!["contact", "feedback"].includes(collection)) {
throw new functions.https.HttpsError("invalid-argument", "Bad collection.");
}
if (!recaptchaToken || !(await verifyToken(recaptchaToken))) {
throw new functions.https.HttpsError(
"permission-denied",
"reCAPTCHA verification failed."
);
}

const { recaptchaToken: _omit, ...clean } = payload || {};
await admin
.firestore()
.collection(collection)
.add({ ...clean, createdAt: admin.firestore.FieldValue.serverTimestamp() });

return { ok: true };
});
Loading
Loading