From 2a85d1564953c8e018dd2d7224de71b162821bf5 Mon Sep 17 00:00:00 2001 From: shoverbj Date: Thu, 11 Jun 2026 09:55:28 -0400 Subject: [PATCH 01/50] added author creation automation --- .github/ISSUE_TEMPLATE/new-blog-author.yaml | 36 ++++ .github/ISSUE_TEMPLATE/new-blog-post.md | 7 - .github/ISSUE_TEMPLATE/new-blog-post.yaml | 28 +++ .github/workflows/process-new-authors.yml | 196 ++++++++++++++++++++ .vscode/settings.json | 4 +- blog/2026/04/27/western-camporee/index.mdx | 2 +- blog/2026/04/28/railroading/index.mdx | 2 +- blog/2026/06/03/spring-coh/index.mdx | 2 +- blog/authors.yml | 15 +- static/img/blog/authors/benjamin-shover.png | Bin 0 -> 343841 bytes 10 files changed, 270 insertions(+), 22 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/new-blog-author.yaml delete mode 100644 .github/ISSUE_TEMPLATE/new-blog-post.md create mode 100644 .github/ISSUE_TEMPLATE/new-blog-post.yaml create mode 100644 .github/workflows/process-new-authors.yml create mode 100644 static/img/blog/authors/benjamin-shover.png diff --git a/.github/ISSUE_TEMPLATE/new-blog-author.yaml b/.github/ISSUE_TEMPLATE/new-blog-author.yaml new file mode 100644 index 0000000..93e7d3a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/new-blog-author.yaml @@ -0,0 +1,36 @@ +name: New Blog Author +description: Add a new author to the blog authors list +title: "[New Author]: " +labels: + - blog + - new-author +body: + - type: input + id: name + attributes: + label: Author's Name + description: "Name shown in blog posts for the author (NOTE: If you are a + Scout this should be your first name followed by a last initial, no last + names please unless you are an adult and want your last name shown)" + placeholder: "Scout A" + validations: + required: true + - type: input + id: title + attributes: + label: Title + description: "If you are a Scout, list your position(s) and unit." + placeholder: "Position, Troop ###" + validations: + required: true + - type: upload + id: image_url + attributes: + label: Avatar Image + description: "This is the photo that will be associated with you. Please + make sure the image size is relatively small and your face is centered in + the image" + validations: + required: false + accept: ".png, .jpg, .jpeg, .gif, .svg, .webp" + diff --git a/.github/ISSUE_TEMPLATE/new-blog-post.md b/.github/ISSUE_TEMPLATE/new-blog-post.md deleted file mode 100644 index fd05fa5..0000000 --- a/.github/ISSUE_TEMPLATE/new-blog-post.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -name: New Blog Post -about: Add a new post to the blog -title: "" -labels: blog -assignees: shoverbj ---- diff --git a/.github/ISSUE_TEMPLATE/new-blog-post.yaml b/.github/ISSUE_TEMPLATE/new-blog-post.yaml new file mode 100644 index 0000000..63967ff --- /dev/null +++ b/.github/ISSUE_TEMPLATE/new-blog-post.yaml @@ -0,0 +1,28 @@ +name: New Blog Post +description: Add a new post to the blog +title: "[Blog Post]: " +labels: + - blog +assignees: + - shoverbj +body: + - type: input + id: title + attributes: + label: Blog Title + description: "Give a title to your blog post. Typically this would be + the name of the event/campout/etc." + validations: + required: true + - type: dropdown + id: authors + attributes: + label: Select authors (use new author issue if the author isn't listed + here) + multiple: true + options: + # AUTHOR_START + - Benjamin S + # AUTHOR_END + + \ No newline at end of file diff --git a/.github/workflows/process-new-authors.yml b/.github/workflows/process-new-authors.yml new file mode 100644 index 0000000..ea3d3f1 --- /dev/null +++ b/.github/workflows/process-new-authors.yml @@ -0,0 +1,196 @@ +# .github/workflows/process-new-authors.yml +name: Process New Author Request +on: + issues: + types: [opened] + +jobs: + add-author-and-pr: + # Only run if the issue has your specific 'new-author' label + if: contains(github.event.issue.labels.*.name, 'new-author') + runs-on: ubuntu-latest + steps: + - name: Checkout Repo + uses: actions/checkout@v4 + + # 1. Generate bot token from your GitHub App + - name: Generate Token + id: app-token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ secrets.APP_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + + # 2. Parse the issue template form data into JSON + - name: Parse Issue Form + id: parse + uses: stefanbuck/github-issue-parser@v3 + with: + template-path: .github/ISSUE_TEMPLATE/new-blog-author.yaml + + # 3. Process the data and append it to blog/authors.yml + - name: Process Author and Update Files + id: process_files + shell: python + env: + ISSUE_DATA: ${{ steps.parse.outputs.jsonString }} + run: | + import json + import yaml + import re + import os + import sys + + # Define file paths + AUTHORS_FILE = 'blog/authors.yml' + TEMPLATE_FILE = '.github/ISSUE_TEMPLATE/new-blog-post.yml' + + # Read parsed issue data + issue_json = os.environ.get("ISSUE_DATA", "{}") + data = json.loads(issue_json) + + author_name = data.get("name", "").strip() + author_title = data.get("title", "").strip() + raw_image_url = data.get("image_url", "").strip() + + # Extract image URL + image_url = "" + url_match = re.search(r'\((https://[^\)]+)\)', raw_image_url) + if url_match: + image_url = url_match.group(1) + + if not author_name or not author_title: + print("Missing required fields. Exiting.") + sys.exit(1) + + # Read existing authors + if os.path.exists(AUTHORS_FILE): + with open(AUTHORS_FILE, 'r') as f: + authors_data = yaml.safe_load(f) or {} + else: + authors_data = {} + + # Duplicate check + for existing_key, existing_details in authors_data.items(): + if isinstance(existing_details, dict) and existing_details.get('name', '').lower() == author_name.lower(): + print(f"::error::The author name '{author_name}' already exists.") + sys.exit(1) + + # Generate unique key slug + slug = author_name.lower() + slug = re.sub(r'[^a-z0-9\s-]', '', slug) + slug = re.sub(r'[\s-]+', '-', slug).strip('-') + + final_slug = slug + counter = 1 + while final_slug in authors_data: + final_slug = f"{slug}-{counter}" + counter += 1 + + # 1. Update blog/authors.yml + new_author_entry = { + "name": author_name, + "title": author_title, + "page": True + } + if image_url: + new_author_entry["image_url"] = image_url + + authors_data[final_slug] = new_author_entry + + with open(AUTHORS_FILE, 'w') as f: + yaml.dump(authors_data, f, sort_keys=False, allow_unicode=True) + + print(f"Successfully updated {AUTHORS_FILE}") + + # 2. Extract all names (including the new one) and update the dropdown template + all_names = [] + for key, details in authors_data.items(): + if isinstance(details, dict) and 'name' in details: + all_names.append(details['name']) + + all_names.sort() + + # Format list for the template dropdown indents (8 spaces padding) + yaml_lines = [f" - {name}" for name in all_names] + replacement_string = "\n".join(yaml_lines) + + # Read and update template file + if os.path.exists(TEMPLATE_FILE): + with open(TEMPLATE_FILE, 'r') as f: + template_content = f.read() + + pattern = r'(# AUTHOR_START\n)(.*?)(\n\s*# AUTHOR_END)' + updated_content = re.sub( + pattern, + f"\\1{replacement_string}\\3", + template_content, + flags=re.DOTALL + ) + + with open(TEMPLATE_FILE, 'w') as f: + f.write(updated_content) + print(f"Successfully updated dropdown in {TEMPLATE_FILE}") + else: + print(f"Warning: Template file {TEMPLATE_FILE} not found. Skipping dropdown injection.") + + # 4. Open the Pull Request on behalf of the bot app + - name: Create Pull Request + if: success() + uses: peter-evans/create-pull-request@v6 + with: + token: ${{ steps.app-token.outputs.token }} + commit-message: "feat: add new blog author via Issue #${{ github.event.issue.number }}" + title: "feat: add new author from Issue #${{ github.event.issue.number }}" + body: | + This PR automatically handles two tasks: + 1. Adds the new author to `blog/authors.yml`. + 2. Re-generates and sorts the author dropdown options inside the issue templates. + + Closes #${{ github.event.issue.number }}. + + CODEOWNERS have been automatically assigned to review. + + branch: "automation/add-author-issue-${{ github.event.issue.number }}" + delete-branch: true + labels: | + blog + automation + + # 5. Comment on success + - name: Comment on Success + if: success() + uses: peter-evans/create-or-update-comment@v4 + with: + token: ${{ steps.app-token.outputs.token }} + issue-number: ${{ github.event.issue.number }} + body: | + Hi there! An automated Pull Request has been generated to add you to the blog authors file and update our submission dropdown tools. + + The project's CODEOWNERS have been notified to review and merge the changes. Once merged, your author profile and dropdown options will be active! + + # 6. Handle failure / duplicates (Only runs if the Python script failed) + - name: Handle Duplicate / Failure + if: failure() + uses: actions/github-script@v7 + with: + github-token: ${{ steps.app-token.outputs.token }} + script: | + const issueNumber = context.issue.number; + + // 1. Leave an error comment + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + body: "🛑 **Registration Error:** It looks like an author profile with this name already exists in `blog/authors.yml`. Duplicate entries are not allowed. This issue will now be closed automatically." + }); + + // 2. Close the issue automatically + await github.rest.issues.update({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + state: 'closed', + state_reason: 'not_planned' + }); diff --git a/.vscode/settings.json b/.vscode/settings.json index d24dd08..e2b5f9b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -18,5 +18,7 @@ "enabled": false } }, - "cSpell.enabled": true + "cSpell.enabled": true, + "editor.tabSize": 2, + "editor.detectIndentation": false } \ No newline at end of file diff --git a/blog/2026/04/27/western-camporee/index.mdx b/blog/2026/04/27/western-camporee/index.mdx index 158ccbe..569038f 100644 --- a/blog/2026/04/27/western-camporee/index.mdx +++ b/blog/2026/04/27/western-camporee/index.mdx @@ -1,6 +1,6 @@ --- title: Western Division Spring Camporee -authors: [ben-s] +authors: [benjamin-shover] tags: [troop-303] --- diff --git a/blog/2026/04/28/railroading/index.mdx b/blog/2026/04/28/railroading/index.mdx index b5d463e..adf7b51 100644 --- a/blog/2026/04/28/railroading/index.mdx +++ b/blog/2026/04/28/railroading/index.mdx @@ -1,6 +1,6 @@ --- title: Railroading Camporee -authors: [ben-s] +authors: [benjamin-shover] tags: [troop-331] --- diff --git a/blog/2026/06/03/spring-coh/index.mdx b/blog/2026/06/03/spring-coh/index.mdx index d942b65..09aacee 100644 --- a/blog/2026/06/03/spring-coh/index.mdx +++ b/blog/2026/06/03/spring-coh/index.mdx @@ -1,6 +1,6 @@ --- title: Spring Court of Honor -authors: [ben-s] +authors: [benjamin-shover] tags: [troop-303, troop-331] --- diff --git a/blog/authors.yml b/blog/authors.yml index f868a9f..7467ce3 100644 --- a/blog/authors.yml +++ b/blog/authors.yml @@ -11,20 +11,13 @@ # * # * Optional Schema Attributes: # * - image_url: Remote path string pointing to avatar images (e.g., GitHub profile avatars). -# * - url: Web address link to an external personal portfolio or professional landing profile. -# * - email: Direct mailbox address endpoint for personal messaging inquiries. # * # * @environment YAML Data Configuration Schema # * @see {@link https://docusaurus.io | Docusaurus Blog Authors Documentation} # */ -ben-s: - name: Benjamin S - title: Assistant Scoutmaster, Committee Member - image_url: https://github.com/shoverbj.png - page: true - -jd-d: - name: JD D - title: Assistant Scoutmaster +benjamin-shover: + name: Benjamin Shover + title: Assistant Scoutmaster Troop 303, Committee Member + image_url: /img/blog/authors/benjamin-shover.png page: true \ No newline at end of file diff --git a/static/img/blog/authors/benjamin-shover.png b/static/img/blog/authors/benjamin-shover.png new file mode 100644 index 0000000000000000000000000000000000000000..8c3457fd73e3233fde22dd5a48f9a5c0a8e84085 GIT binary patch literal 343841 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�`F@AG7hW#;hQp74 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

xDB9MnU6v#R z(JM&Y1>7BHO*;x;hp+@z%#`V3k0stucAOu$IQz>=><88ZYXb~iD{u>Wf%c45!^i6v zXq{$MMKWu3L9E(ls*N3*u1>i0&KQr?6Oy$p?2pU>6o&s*O%`yB$krPipY5NodG(EsVkcVG7_DByAehtt zyBEKM)zu zInUYqzOT#teM4(~R)NOY(oukCkH?Dt_{BlR9x~N_O?3HS@BGGf{Msjp7Z3iqTn_%= zRqo_;#wC)?l0_*wd%AK7(Psl~3d>j=-={AFMb09BjQ z=Go5#H<26ISWf^|K~FYtds5{ho~3;g#F|Pfcyjnay=?gC@s!%(=i9Wgb{RVA3}{KK z8?7F`cZ~t7nL>{VnIrU{h4gDZ(`pSE78uM^m9TNjE@Eq{e`^w?HeTroF4E1yEPxic1QM6^m>aQFA>v2pI0FO(hca5ohDc?>ALVq8%SN z6NMJiY|pm^SK2qXwQa+N9vK_CpeIdw{3^J7k^BW8*>A0T&vq=cvWVGpq?niHLurwa zL=Ms{$-kqT^U7J$oZ zhq`B^r&DTDr0&WOfr`CHHzS|w9mQXi92_j4G8A?FNm-w1eR=Ke->7KZyn>!0k~+4x zL4s~G;jVdmTFWb{b{=(ZfZaPS{?%!1-pAECh8l@-3WCd(q+k)LKZik$Nt$$d2Y%jA zT8tYR+7HiiNGBFhx4h18{VXIUU5hDED6L~SD|*Rz22w%SdJZzdfh{fp$A-8mq#u1R zdts|%QTy`64(^6e&9^sS%|yMwO2S=DYLaGoeK)Bg~G|E&@A!D0dR3&Tir z`(y9&6X*EbN{347=ufupaN>4S5L1$!!f?6#F}Sv@BJEemtTUY z0ivP$D-cHCrsL9#taQb=G!&9Z_AzUI>K=8vdYk7|sRV3pe)6kf*VXZU(QH#|NPZHD ztT8{luGZd>Rxt^`4;C=2J_v!wr?Fz9sU+_?T5UVO-+mDAzb*ZQ9y z{E|3ZohX~=?DoRep#8C%M=FBf9wJ32n~+s#J!}P8pDCL=zw-mem=REO(XN=|=O0@* zK;|d@Ir0&Agt2TU)IUH9UCfC3Z8Kg;ZyUBPsOU^iGm2}!OsNVuRfW=ay$EV|jd(%K zt>GoCFN^S_7TK0VTpQ~O0VUM599gTP0TM$lr&5Q9DIsthRGl@jSn?T1zvS=LfEDu# zirJArF}EaIzcoFD9vYuYI+i|b*v--dPQqEH?&qq?w3TI*%EsOtMI)TA>a_GSjK~`Og}@XC*?X11nGONLL;^dqz3jw0|yDewWowIYF+SpuFnxy1-%;%pSi`%tHdkS}&g3Xu8$!!V zA_>eO7spSlUTli&-Sfx-Xz&K?)@5Y)pbjn=}@b_2w&>=COmO=^SEu!IDqCjWI=NLSdH{9D|0n zXE%IzE!vga2$I1a_?GFPtc9O2{uH6;4$4AYT!N2vfuDh}ry%!IiT3v4d9i&T{Txn5r9C$fuIV8KJ#7@U%Mp?3*}RS{k)&(C5uYfO zN)JFyc}`~mluR;=J=#Uo=3hpiFCeQZ7&wN%K}vK#u>rKNzgH36@f73PxIdq;t5Oc! z#f%5d_=|IkgPEq^H@;uND=jU}i7ejnqKcSDX<*uHJ=8%-DaMVE!4LmM?P#x@^jBXq z6UUq`mZ{z6vv!KKvd3vjo9y_b?aGVgak?1TCTFa08*^3yyXUc=Vqm@~QYRnaBI z^MZx&fvuNlXMC}Wan}pu0o`*#572@Qf{h;XAnV*di>?y5Y`KlK+>pt|v(hcRjvG2d z-;>B<#-UTk$M)9%{;o3_|@%wgX1u=KKZG#0+c_pj6hg1>Qx@!wV*DEz?K z=w|KU{o}1iKu=FkR;l&8T?5E$)asO{k;qB;&<#jR^0#uARLW+^#0L2})?ErTWD>9V zJqfJ%-Z6?RJ3D(10|tD2A#`NXJsW-T@Q5xlBgztwhy(<^KmYfABgwe~%rkeAl!4X2B&i2uXfvvMyzm zY2)-?C^)%zpvBb@=hr&maLu75B~wSMM4#;P%ELdtc|ejc(fZw}r!Vx=VEcTE)!N|` zZx*^I#<+oR*V-D=t`XI(r{u?_&&Z`y(DMOo^8uUWwJ^VXvc9+8o8prC^htOy*?~_| z+FJa?P9BX^$Ou#?;afFia1OD_G+H9l#V-SQ#BQ(Ue>AoH>bqMIy3Ku>O^bC-b@FrT z+qy&B*RVAqW|6t8GnfB{U0R#})%IO>b=sJqAGb2P;Da95yIrfnt~ELYnv!ejNIaV9 z;Q~m?;>U2)H?WtX2nhT>m(c~F?G2-Qm<(St>viQchmgRIN>J{eUp0E%Uv%77-E2EN zrHptS%~#Ap)WuctroBRUZR&7(j%d%h`2^PfUVqX`6P&hxhhNudspPI$o5&q2Uf4SF zeXsvuOoa}y->5=qA4lYXIWh|n28~$E*s_(9o5@%<3b8RyXeEwt`OCBf$8OFKUzUaW zXv*c+1m;%rTspf)j|tiI?{9|)HNH^8mLeFk`e}N z$};%jQ?vP#aOOb$xyg*!6SLpvM!z?WhBsHg46Q|Xc?JXTFrID2JAF!*zA=14Jp4-j zlaB&CJmR*-KW5aWb!W>{=|Nwibt(2{L*Q({rbq`NJ(8>xhWCF3o8X;s;I1t z1Oup0C_Zd}SLe{MJ0D>gnUmw=PG3PwmiRCKy1oI3h2O_l+9`lj`(eN$ML8K~u?@F^ zBk?4yxKg_!CEux1>`sG6R%S0;%N~x&dr)L|S-7fYOAsUBnS(xSmgmYltT1u@xHC`Q zKjy}hV-cYfZ>5gN`u)h)qVawD0IDQq#Plhop4|r6i>M;0g;+7;R@$!0ZW9Q)9(mUB zVRr;2I~MTeWgJ?>37GG-gv}1`%|mpW^GA#xOBU31jyY7eA~&!-x_rI$_pKw-EPUE#-Sc3 zMf-!%Nth%%0Qp^B#*SLZ;`jgZdHOmsLjRveW~ZvZdY-tyI$G_M)6EIx$QCmCn+tFU z`KiO0KA!qTQkg@~U`a`nW2Mcj$G4eGTBSygqM_zjJ_G&jZv2VozgT9(`}mdnK8MaX zo2}3r-#+)mTD?PXWv*@C=8S$wOtA<+;rv$40GCJl*nf44@<#RF;)~}GuHEjMH3m3l zUZOdTkaue81MZrW(sKD?ZT|neQK2f?uu0 zQxRZ=zAV+KT~@Pec=xaz%uP5YYpE7u!p%z&@c{)v^40PelR1nqN~l$@$4Yz89qB#u zBgmg-WZboavS%DbUG%%3Zy6{{eZUJcGw+0~zbs33uSJccLS?lFXjd`g&b3L5aYOzR zE~y`_sN-m4$5s_ZhdK=lfQKxvfjFbr_%4CjCA5nr`T6J7Ni}NtC^Xge8UGU18Q~b)0k8D z-7#Z{f?17|&?S+aFxRYk652j((>*+X30jDLgsp#M|77@fYY{()gE$W_K9sd}CS#^2 z>%jRdTj(Gfi@bbx>MQ?G%FS2PbJ1wU`~eryldfWOYUQ(U`BB`IBZvI7+2s&WeEjNW zUJVy8A=h!#$a+*V$H>-5(kV$;U0QeJ8TxhAYBJ-?_y6rIfvt)|>wmO;;iuR7y#ukA z{VS;$tumgvfT0DJW4P5Z9~%hfaw`Giq$cfccH8HV|B5@$$R<)M%Q_*~Iwn>8mI`F{ z-RMOq`ZSYD_VCpkL1FwaIIUI12~_V7oUy87gV&MG=Z1I6lEaP=zPN|XtRn;hV@tw$ zj6oc&!mDgCB*}q+^8wYAOk#Okd=$xYP226W*Bd9n3?q{{7Hrwa0B<&QmIVq==%M5T z1178=V)@={M1@RDMnS{w__{lHjCjr6S&C?7WCl?6D>SFw^zp)_Vwnk9LZ{( zS@n7|BC_D%0$rxgwB2TIdkJsC&-k2(_njU?=8lx`di`Er9g9y8_a`qBgYVL}th?VN zr{aEO1YWogIky?r=cual1Ucn)v53>;2kBMe5fLfybZ}G0zElbwxFPmx2ZE@Hy8mQP z12<53s}I1FMD;~pG!#~#NHZ5z>UOhq>-1Sc`mbKJKdm8b$Gxq)1RxkWG>hMOHC<{s z^DjO~|K2@e1ukY31R(SKHt3wuLg89*Ulyn7d-ZVjXIa3}&22RI@tje8!O;=2b-lSc z)Y>A1@963psG@U9mP()Qq7loR|8yO-NK#$T%N&;2& zyGD<<#T)Dj;|3=7Qu_3PWahO$zRN^a3o3Iz6M4}QH^pi`IRzn9l$=iP(uxZT!s2J9 zI9AD!2#QHp7zd`sJ$dmZ&ujn4Q*YXAHhdx|l_?6j`j)YLaEXr^wcNmt5k->Bt`4ap z+Ppnq#-ilHD^roWNaYXfryIn}H{*|C9S_%+#2%WtEcUWOGJiu7U?(>~l2S$UVx!Xb z%r{wLqv-wO`l#3cF1%MUabsg+Z-x{Dh9^%V!jD+R z@~oy>+BT%nBh?KocQaDYPr>6f8ONyG=c`xA9W8SqIDy6f_9t=SrCh2hrN z4Y=2KtA=*Yx$4K%5P#i)ajr8i)OV}Bqm1jkB8rmdm+n!*JsfXg7G|@jx8q9JCH(0V zH;=E1Km${Nj^WUFWvo zCs9=wkrR&44ysR~jm%*)K|{5$I(Wz~ml(&YGFZ6+#W+B}hK4iMR_e@6(ev;L(a8Ih z&qCv&=RU8Dzg+s&*o95$h~Vk{{BjhLY=(ZV*tIsyYWC34)0sRPTYrhPbeQn{=uXUu z!QP9(fR@#r_xPA1*Ffll0_FAy!e^4ork2OShaL{`YV&&iZ$Hh{C6?4zwJR&Dz^b%w z9`uEz?aJbsgsdZh`eBm;B-Yi`wGZAQz#iiW#D@F>e0m)vekpg4nq5?&o{@ecst8yI_pM z1@a1x<2|FHEQATg?A~p3J3CcgH@6@NGC*|_Z*cTw500Z=5x#-cGs!ByG1)+_Gw=x< zD#O>wb!d8r(uU51nLBQAY8QBFd)UVSqxzDDh(zDf`!ow8(7702qY@oTgWcSRb~nXq z|8mMz2L8s(OIBNpGc&Z0-?WV(of(-kGo$!~wkCFlV&m|0586t==EeYmpuT{kN5}T4 zN2>4BlFxEm+xmEbe-GEnueG><`!^R3Phn3qa5sv=E5F=Q+6h{IY-{G?ioUxV7X`*V z(x32yLjBpKVCmnJyGT{-_krJ;gK_piY+K7P&E6l|^lLw3AW$XMhk^}rEnyLVX>su` zgD`$Vp#Wev(a_L<+|}ldUfWZr;gjYQN9vK_L=rVgWT&c?YLGoQKmX=T8D8`UQMGTZ zuBUo+`J%Y!=p=IA1X_yC+m=svZ)0Z>+0Gv(Zfs?A2e15;*klP%dUpdjGxLdL*d&nC zs~OEx_~PU{wNlP-;u<~dwr%-~2&e>JROodjnI-D0uU_0{@pr7=;4veve^KDD8lU2P z-Eo)tl+`*Sdpi0?x6$gr$=A96WS5<{=#$TM5&m0AWf9AeLNJU40pj3yvu?nbWl_7a zwWsNOF{25I^_$|U5B^i{dwIWP10$a9I6z8Xk-mF zvd=dhe_G>bVE%1uB$(^s==g~!?EYpvVB3ll7%`Ia2XC$)8O^mZ!vei6Y6-qpX*1!u z0*lv`18aqq_e({r5OP38K0C7{A|+uZLIsFnN_BORjk=<;w$2E}8)pLSWo+?K4q%Ux zT$frucF*K!@@lD1DTTfZc6J#kF~Jny^cpiFD}FNqh)8GnaRZ7Kvt5pZ05(y<0T$Jg zi)qIPjVo+KmqTYnwtU418zmawkC2)fW2r0YVntCcRPOy&EZZ;^20`IQnR%`1>9#5x z$#WDW;-s;^CA=#j^>m%Z6eAZ#Crcf#Sa=X`ZT?Ux{+@eC8T(cDdqlr-4KeFzJTI{a zL}rz&4(^SIE!~tdNv5jhyriwZ+PC>+RB$kBT=&1lzKlKy--yQ>9|I}3Vr!chh9V^! zO^TQ$Bwt(y#MnTqME}&}ljQ zQXV}&Vc%KVH0-?&xMO~zuL0DGa@t5Jtu&87E18rE?_1@xc&Z=h%+Pp%D|U5>VSU4l z__utJu7+XOe)0ca@`Gh`5W8Qz7 z60}LJPAjI9pK)|_1n=5;)`YiXHKQ&BVcD`*RJVw_rfDWZm{UTlkMr#Z{ z`m`He2pvY^ap}iR0U7U2FcOyTon6lUzH($*;TDSYUO{2ZidcETLnWuQ6x<+CBq=K^ zEAbWj@f(E^gQO5;^(C?I)WE2D#E{H5%;md4q z8_A;P_lW^Te(#OvZkp&+87!AgKEJl^NFU`?ariqtKOEVTvoGHDW!3zcL>wxwv@M2J zbFZ2a;O&g3E1=;Qn=%RcQ;f(gc)De9r_`J9X0s7RdwQmw4G8%S0Fx}zM({~j+FX?( zu>5Isg_X%xI(#0aPxuPrI3o7e|Dt!snXmiC{kyFo-%^pgN`L4s&i>SMuqI@^h|5na zEp2LP2DQb+UN(oU@tuIMwEDED=39SuULw>b3rg^Qhw%!-ZAk^2xIVM%GK|iCX)+^} zHe8t`*k5c(12Yld>IHM}$Cna$$%=E*&p~r4fAD)L@`eccVx|8_Ep_VJiG13@Gbja~ zineM!{#o;vdLKGf#+@csmELH2qr17_dVJdua-Y(t9(#>HqpS-ve2WD&iEPVfXz8lRQI5*e07E?P*zp>6j z|G+A|$#?y!sg1k7P6AW!Pnd=o+c+;T&-Oy9;S_CDEWdjnY8iMrq>z8XkFs(n>#^c;b`se+`eW}G+4&3MXNxp)SO&lR#iR0VsPB2`-9IOs{gvxk+93Ae z()7$JvVL1`eQYPYN*{TEyb@G8$Ssi0`VS3}lFp+4Cfx>Hsd67ysqpV0m@7ldj(l6Z zsSSm!&NW-jQbL4hH@x=nK%C8)hEVQU@H(9~Hsu=G8YUTw8rSUkh;lH1tv=+PIjK#D z39Kq|1C+*kJL!c*MpW%1>AyAxjg9dqWKF?E?>KKMU$fkwdX^=y|zMAdPtf&@K$lFMM){b_yoL8Aj5^LH@Y8sR;n~RX0rx z8{r3=*XMp9C@l?*Hzxp7fJv*~{~nD?QxF_bv0>S*)^^zC9dU0346ejWA8;4IzwK=c z2GDvqA!xF4^R>n6iJCXJwo;C;SBHH4HX$=JBK-77Jb8iUID z&y7SxBwuuoFRR9{9!fslEE0Dt$5C|Dy>*v@it%S9lPjp)CO$VH%6~ap`6plGubrsD zep6dpaiuLPnnkbipm(!zD$cp2-vqLvFf&dd?B<3pFG|rCw$)v2lt7am64$TO#B?05aQ%CcD?*1c7l$QFr9z=P}Z=L1`7ok|Z~_vqR(C z;ydm*zoXf4Pv=Bbt;dAa6JJn3IaN0f_9%9mLw5vX)7f@krd+>hkgHRptQ#oKa+{*a zGxpYxxg}nTS6K5V)qIH`ES-wS;VG_%=*u^Z<|3PE?03?l5Es`^+l#CI_uE|4Zw*68 ztBRStev%qT}wlG(PJ-8aIz1u4Q6r;YW7PGRaB@!32A* z;Dopxx@c?RAmvay1fl!BwYbRsXu~9LzIS#miqJuNVXyTF24Q}S(n;}oWX738g3B% z7*g80e~27)t;A0N^5b$?S@}9SjFq{=b5t?lQa0ZoElCpp%BnPy3{|T_1Gw}O`b@}U zKi<83lRnz!eersrq-1JYE#(3<1ruuZE2{gI?A(9(oe8vG{FIlULsXWg6NeF~{XLse z3QUzm zTz;WV5Ds*Xc&&>X7dPn-wh|?Cd#v~)QbWWER80C2W>ezX_L|wT`A1$^TFaxI5(iEOjYpRB! z#aNG{WhAc=a{BSJzgHpkH(^5eU%|Bw(gM=T*kD*<6{A?v<&2i9-ZSBN~ynNO_ zcg^uSn0v!u*puI)8eK(2U7cTWc>^Jm3@ZOw85<%miB3(Vfaa;k#GMxXeK9iNVXk9i zA!_HpQ~F*BN|>kyJzXTcp5?pd2v3z^$A@zcumuB2fuve<_|d%(vjIP2?A_ci=Hk1( zYvVyH{-pX3C_iHIxCk6?_XEp+H??;Z+uQ2Es~0N24~P0^eXFNA@s+^{4ooYr2d#Qg zgZUcyYx$`C4)_zDynV;Qu2RJk{29fLvAS~l_9JD@kiV<w9!CEH-t6>HH-|nN$7}Ycn;AnBqM%dwboX z_@xd9b#(!9p1p~5w@l2e99;gJWr9eZ!AxRY`#hWX>tuyJCd6~yqIoY#c}17%gN16? z)O2*xWSSMsb+K+$6r)*PEBw~O!&^-yiEg#-JQ507-@t~)yW&oruMCBld~>cmo}PA3 zuaI}!4{mQ9PP1MPI2Z@~;VF1`12yfPPw9lIX@}lM89qt62LycwLL&JJIit{&q zZxWxEfCmO@KlsU#>|Fi%vsA5LWtE8x1OMh?JDXA&+I?kA%o3A=8SdMr*^KZV zj$Ej~6z#g!eff-&rXKD~qY81S|J?w{62NsLN)CCs)mx z@LTk{V2LiMsFoluGfYwN$4>f1HQiagWb)SiE?S2qK}glNilWu`ud`nl;EWrznX#lR zxpDNXsxdK@0zv4}+Fi!po^3-bwKS2@A|`2%ja%3c*$iGuw|DZ{?8K<}Xb7}nB>7U# zl75W23pvE^-<&p|uctAL{QaXxb^cnIaAT8N16k43tp88Tr5ASOf(LByo=F~|oKrXO zXBcVtYh(2}T4obozWCW0qc1x^_0S?>3a0Yr0s%=2DJ7tkaH`+GbWop|Irzt|%|O*m z7d&4S6WnX9Dkkieg%F_Y-)Y8$`|S}0tW0)cxE$Clz?_55eZ4n z(jg0Ru=LT72UkzePJm`>gMez)lq<0`RAm;Y=P`c9JLbrGN*EMJMVaOMLY>L{EAQS( zCfF|=aQ-GeBDh2dL}8qkm!6Z>+#ATG_7BGXn3}iC=Vu8;jI5c>=Z`zFi@bmcqZ@;| zcpp2HK^s|>i=aWk`t2=Y0oeDHulPgAkg>d#DGqEO7QXmFNR+S%f`pMPAK(pOi2$hP zmu$zPk@?+cFR)2w%~!p7jcON^HBFWSTs#!k4OWIh=R&eA9+@cA|J5F>@9atOY4eQqJ*;dZ2xsMM zrvA||)#;un`N%ds-toAiC6vUq6eVf{W5s`m0{mLUo?>O#(MKG8Arqp*;xBz@qE~{Rr|#8Dq?R>QE_)8zaXhF1f&-AldN8Is}Tf~ z`_t}gX`^HI!;n;Weq#*ODBX-jh%Nil-)o2Dyg$aekM792!9})y<%wM1tt+J6(VzC27@sIc}ZXKooQPCh2`o zK%y&uZ zn$+OP+ktU(e}=r_eTO^x_xOt@2)vo|seL~wh#xYe04aO~#j}$Q$x6Xr2ymko=tM2U zPNNlES@Zo1qc-0{JZ~boenly(75^wvO~k8xsopAxJZ7m(Y)^yz?$zZXJG6DjtL6u5 zsgLsyR*BH3XZkd+DtBEaw7IO9*Zr$|(L`t7g`mFS>H0lEka73J z#k(qSTRHI@e|TVo&p>H0u|khJp1^yOoc`SXW*IYL;1}2iW&7PBUxO|A-gHr#3GgY^ zo~efR2sU!feQrbpBUylF_rGdgQBI*|HkA1TwcdOJ^C5X4d35O+NoC%>pt^D#_a%-S z@V!3%+LE$YVm&-G6cPRQUk0!>HFDDd6OlzSl(%mQ&@d?gLx*nGRci$o2H7~Xu1(@Jtj`aI$Pi2&^1Cu9oVUjdNxrXTH zCN`#4)eIUVG-3tEN*bzoIBtxus~KD}B%pNshP4LF*6@^?!5rM#19w^dk@C^2U^H}3 z^0O_GS)P@WkMBRNSvyHDnt{K6HSGDU%p%yJ)Wvp?4^)v)2`b|Qd0yECCHSzKy&f!T zjUiA1PhAf!YTNQOf$KVZdHid$bZ+s3mDE}4i~T5h7(}F8S)r<2IBz#P-T<(TFII-! z{O@_DV-qU$%RCGnooLk^=B?^|6QsxOm}Ut#Ine>Fz_9zCDygDWlE0>;51RbAQ1+kZ^xp6dx(kZ5Xyx5_Z6 zxd>w2zN^36bMG`$wP#_+}{2{FeKlXj)`myK0A0a z+YsxBL!IQ-W7BHt?%Hp~pC|f92;BIbpi^$d))}WgnMZ#I1ORs-tL%ytTt$@U9M%~u zBk2peco8B8OXLJ2j4UqohyQk_xf0)@2#ok=Lgq%;^dxpe$>0;yK`Z(rLly zvI^kq89uL5{Eb4WfoSk1}=dGmv9?#*E{Lh643PoEZ zseqr9PwP#AHM-+c!%$FZ3&hPaM?S#f$3`SaCD&9sR0}Qr!NbF2Qmb#Aq(x$2pr1g6 zlo&*(oCLCYQ3H}1aGVmFomK5vR7I_$S>KCW`izGE$ZSQcuo+2-ha~^;K4>zJj5d_k znF>Rg;*PlEXj?IXUE!Gr>YEw`(EWm5Dl7MH9*5H&p6qU?ENbx<%_rl)<+dm6;SpPz z&&}&)>z|u5WLtkP?XG9r@}T1l9L^{vk1hHlEjt;XOzngYgW$-F5LMY$Tr(pikhM02 zu{!&Dw=?vIn7V&d++>rVnTk?fEcLaqY>=0SA;K5V_{-Hzl$Ln}!$VTqdi(QPlgtk* z_bmRXhq$9kYtc_VJK#$qQc+E?AU;%AN%x`4&)o@=vEyDIB89w_dPuT%> z?HU?770+ab9%zel(yDVr-JKImP!S1Wi6<#HeD=zk-}KYEA8Vex4C7-=m5-vf9;bNV zV;xVgbKwp5;gDBGIEo>#D=NJCt3IV(pGVNahUb^kwuZI<;8^H+sA%k%0=`0tnm+w! zNKP7&q*FBO>}yekY5ACXpM}trG*a8Xi2gcHnQDUyt+5?7w*w;G!j7f-n5Ge1BqQ9Y zGW4RNQSNL707s(k844ytu9lf37~veJ+rwl=?44^!a=ogd9dswSd$w|R<`QcWrY1h* zOpb(x?dZk*9}j(njOQNkDZM2;^W>+=lZFrF6AfCaKE%Yz=!HBVm%CnT0?< ziaIK+IX=MkOBH1JV>!mdJc)iRT5(o~r-Z{q2`BNeDkvIcQMq;+LCm-TrI$5xCL=S` z_MZ&OiuotQ2sx_b^ikAwxe_Zw5ueuLl&d^iEykT`b?Jts$!U|sIklL=3a#)SCFfTG zVg}tZ&)nhem~R^vB5<-U4)=Nf>BMMHjA`##YkH{lGstt^wU$QDuEzopG`DOj`7rI zZT-j3J1p~nK4|U?yyDZX!(aNi2*({F5HyxDq1b zY-H$B)QQy8-XuYfC!2Yu{m?1VL0DZjcKD>Hn6~uofOlih8JM}zft8(_o9K9v+V$d& z@niT7x^M%Wjqsf{F{PocaW(?eoNBZ9KKpY~t>{^d9Wcz?*iBk<4LyDF+2Qa;GvLfe z(EryOY?LuI_w%)@-+0eMR-f~zO||i3lNtEc{O6~h9Vmgql7ZMH}GIqh8DAQnulm&N_}_+o7!J{0ge;*56yc{W9Iq5)|{40vylb0>-y3v*AY^ zgm9^O0hw8zEsF%BF0I0G-5`lsd&(s=Tiu%gYJ<7{hVKef`uqo`_2Rm(UGsTnXDjY7 z3%~6u4W1$LkITOmejmiaI4iX?=!#j+nA;$SQ-M?1+_4{3^yuSq==T+Nc;!d_$8exU zyTmHuDFDdd1Fk$CpLYH0Bw7c0>!a%5V|ayGK4q`0`}A55pY**R*x4b<&Sl4s)3zi~ zhfR?42{9k*AqV!mhLWL}eLWL!hi5Z*XA|Ay)2A2#`}QpTaLu<}z(kd>j&kpxzzm>X zcZoL(1f3;t(2A*OV645?5G9_#o+9e*c%j64}(! zV#Z^b{~S&@yO&((;|iAV(%VZ?X3w*&f0RP|HnI-fM-DD%BK3S_&wLwKsHUP!TN=ob zDQ?@6sA6{)QHifT+ZAEjiw|9EcXo|@-Lw?)cVa%%bFxZ{2rE5NA_`-QtTMl8>r(Vx zfS6G^SmIu-EMLqv{iTuaq-Jxv6!vX7o(q(_a1LwpuNQI@MzK{wLNDtqAPGs;i7urV`vFG36P+p=6Ari#&Q$?5#S8CQ(VE;I) zRRx=!)ywS+Jm#FU`-RtlXxw+7z%XvTm_i$y&b-|#-Cb)ro-IbP`S1@5_z0N|#9?hWJsECaMFsE|zCwTz;H9Sa20fX4J3_B@A1;v(_nRTM?Y{uxtn4~BMJxr?WItxJlEccCSrAdE znmi+7A6DbXHixHXRK+;^~(iWZAuVPC9H;S90P?hN`R~D~p{@{E;4sdHAgBFktRl zhm(O2Z~ej44ADsM^ao7PhOvN{XTMx4_TzH}ouDJk%*`V<=H=EBx)AitLKU9E+vacD zuNUw1p7GI8FG#-C@nFXe{9NzM(~QaVS=gYUpTi;Kxh;{+g`c6;Y(oLZ8`CZP*IYYp zwR}Z=nS|!YsJTxF9mH}pa&Z<}S-DQG-ZJyDg#z&U*oXP*5f3%>gJ189D13BGT$SR_ zz3sSk|NRFgn$4{EO`mZ)RQfwZBdxAjl-`)uOGlN9u|Hi=kp>HV)oC_OK%T3CoE7hdhU$&WVUZz#{T={xS z$Mu1TbM%$;}(ot_Y&7KABMdyEsZ z%!WfGY;|Ch_(4+{1|5n45cC5b*f6Rp9m`bzPh4Puw^(_eP{pb*scwed%4n~;K`)-} zVD81D}4>S$n4XbSnB(m8X@Jn!k+^e zR(+poHVK3fm1TS_wi;6UOu8PH7@ejz%9z_%H#dVbK_koI^KsG#X107*d~``&#ab^;B|wh(=#0cR8m4>T>avrqn03_}ji4wv|Bc6Pj-oW80= zw!(EK!>7Q^v$Cqn3#__4t~`R7oMm7St*7 zSG=Aa!`?+?>Gx+y*l7xAz0E_`A;~If3g_|ngOZ$bCPVJgrt4JX;Ub`(5boobf%};F zG$6F*;Qu5!TvJpIZprV&4WM|hy>fuP-dtH}n_68PHjPtkV-1gNVX!JyK{jdQ%BtGU z&@jW$m}P_?ba)uOBG#t2im2>sz}#k#lAXZmU#CN=P{PH=C8ZWVl+el2pZjWWE^1ZL zS`qcTR^kvX%6&FG5E#5uK1ZE1Z|U3}>F`vB^n4!1iSkPoZ(hizTCuK!UREt*nZ?6> z7RAj3mU6Rf@18U(<=Z9QWUL`Nzmc#{TU#rz4V;oy4)r)SjIXX9OwUU?Tk?NgS~(et z1x)j*SK^;h_BKD=Jvs#JG(QZYi?+zsw9Dd|2H4(oaga@p?Oh`!JhncyJzY2ioRk?l zyLuz%6x;BXR_3Xq2a~%|66Utpm`O#O(b-U+6>-PDj-a9|+lpM`G_75zfk#klSn7@C zBy*PU@>=(`#IIEzC?SI}(76$KFZ-b2O?!04RNI3%?GZX+yoz(fkSckl9C6{ZQ0cVCI;$5SB zbAXLV=*0k8Ym_@bpatHWAta)lz^C{63MU@V>F7KY;ECM`R-DVdhJ&5aK$s8RR6V*r zuPHw2n>yg-0M#j%k7Q|g_tsAQtd*kUPJ%#1&l~G*I z^?teCM>55{H)1n=TfEDnwTL}52-x{x{9~k`>n5(fdP5=)n{o@yFArZ22HLjSOyUwt z8avMay3dV`jnno~J2zGZQ!6PsHiw^-3bGty%G=ub?LK<5@cO%rWeGZE$>^VdlD>mrY%$A|z5iRtdXXpL^O^i7mzorJQ76^MXk1f7K~@2UB<| zN|v1C?9Ngn6iRBAQ&#)4B=A$GN}F+Ry1w`01<`9g9sllcQ#v|7vuoegRv3d|h`)c= zCv3=k7l-E2JzSQ3h80SivXh6Mm6J2ESLFQBJ(MDmbNh3xd7Iwp z^>OFu1+~1o%z5LRJ)PA>ofj(wsn=@e+o4aV`0dQuVKodV*|l*TDP-90#&ZN2Eg0^( zl##Bn3z~D?-|7*u>RkqYVyjjU{^MmOm>7)EX{w9qw-@PVF{#%Zk>~>V5OSfr1x0CC z(vk(eSylm7%YTGyT;^k|7NF62o44W-x-)L*==XpWLNUUw5EmDbAHM7P3O|yf2zUCAV&!F) z&Jto-OQ17ye|~xj1?e!vywKwf-{a8JU2e^yowSeWzhd&}Kh66h4FW4Ys?Gy<`D9p{53$v+)Z`@U_Z-}Q3J^esV z(|75+DH!42MBF#Zu;t@F&5S<>zLI*imUq(*U@75NT*~e|dQf-J2SW<8yQ1%wba+ye zDHr!;0%M4Ookp9&o5ZZFUi+b%g$Cf_) znH5o#m(m$w{3>^wVpwL%4>mN>@X_{^!m@G6$`!S{yL@$%2>!xR} zA-&f`S>rKV8;0$~j>@ZJPR)*mffkkST~9oA&hN(NFRp|S8}X|M08a;S{q0}u6>ps$ zh2EMg5)JU`DDPxPLQ+Udv|?D!Y(L<$M+Hg1dE=NZ>eE?hEwD!kAODoY`tb|PCD_8a zdQHg_A}^*P)twKS9=wHVoUdr#sKk-6AG`%M{%Xx{Ju`R$fmW1}9;(ttX^fiDQpwJ4 zKy;sYeGLW`i><}>6r&;qKbmfyu6Hw>f!!*!VxZv7XQ?m^N}0Xdl#8cT*^nVC1Kl*r z_nq!9c*2b1-H{{}MacB{z2#z>wQLTjJpZybs$vh!&TR0ia-6@De+l@0HsZ8ue)Uym zLSIuDMa^g_#%5=kjzKN(OTtX5Hh5GH0gNOrj)EWHOo?ts(fs+oSb4L4X&)msXdKDLa(0 zt|}rJemQ}!xf2vY&_K|&BP!<_mnu3vjkBv*BOSSeFh`JIG=I`DOWWaze^EAUs4|(F zEbLXSF30%Mr2Sg<*{*H~!#5j~NdmX*FzHZO#eQV=*$EYK8^I$yk34mNWl~LNtn_Upbd( zJvDi@KGXmey+%ABIw5A11lw<>rZWQ(2BPt^kQY|^3<|;3*gmf+bz8BfXZkxPYSYJt zn1{l>{|yA+z#G#(HLxpZHZmz{qi^6h4;0$Iu>EGoLE`H;253bf77l81qqT01tkML` zyx$779qOo*%9s&!7Qzl}W-a;KfJV$*M>s9eV^I2HiifE~=5I!j#60=upD6`N!{?_B zR+r7IeOdhfZgGPPN8M7m3TXy>bg_TJTYD&^brgxdxE*5Kvo|o9ymMq5oG=4DDZByt z%qC87+GkT7cj06!cGR!ZB?z5;cd;J9a#B;=C(!C?Z;;tar)2&yO8x=^C5{hbwKgn6 zGGT>kE?ip+Y7LoL>R_9qyqawv!MXrixJ}pm>>1}J=DL=`ndO5NeWn5~6oYbaSqBxA zrLnMz-|)~VAyC@6Oa&+aU31q#4kB|<9@bj58@Fk1ut01EPan7!{gPlzWZsrAJ%XRv zS_pf!S6*mO?5nRq%%p0x!KRum6UNBvL%rF?VfC`5$UKuKud;~U8#Vv;^SL4}yoNFG zps`i3Kaj6WDp|22G|R5(Jl;7$EEK!4w{$Or`*I?9V=VuaWDi!d1Gzn<*fBaahbyt6 z{@Fe4>cW8gUh&!TzcLJIm+-=JmJxd~M5eo03MV z@A+-ir*zSBXf$RysA}m_wGAhOUn}O&Uz8Vr^U}whhAM8GE2REAGRqHWhjGw%CNbqQT(p4%Y+?m>Y&DY zJEj`%-KWw5h!kBvK?QqfIxLBM+Ecm>;T`?cy*57Z^xwbgZ1|Z*n3n9^`}Y zed^=mKTRUVxA>2ykHK?}x^egIJ-3h7LiHBEj)NagS2~z0WF=1za>J81B!edeT)@14a25fJBk?kD=rn(eIvRSSO)})(_Ji(j=>(1 zR@CB(hr<=0xioyKGX#j|c^jrSqEAKapMIIL^H%)`2RB)aI+R|$KOZFFRK<==exrh{ zj$R+Y*wV6L5$~o~V$P@jTmCEMIrwu;vXg)7*{=bbd+H!0Aj|c9m(Wv(P=LyAxgJ$A z+O@w_;4_PoVk*`;<^(Bz!xi%4Pt$rR|4so4?swGA#1h^dxsM`qfX`36;&4Jl1plVz zr2`NQ2Y-v5zED%H+2>B;=B#HzYRRICvszzHy#1|4Hs~P*W-aV^x;j48@2u7qnE)cq zh&RKHSry*wfdq0cn|f{S+EdJ43D}lP0{QL~-v{->Z#y3&`h}UbZL^+Eq+%BHsw%Ei z>-xqG63#o#Ev>B789Vx3{R<1_*n5VGOXMo6LCy<5@F4SjSQpGX1U-4-mmB!t z!#Qp3Is@*X;)#y~yCYr%uzWzdEJ3dL_!{I`wn4nC*mma{O~3Q@0R!Zg<>lqy>1;)d zevME+_TqIM2MVcygOd>U71toI9>0#4F61c!^AJYGo0ByZN45c8Hg}ComKenp$mK`n z>I@#rgyz;x^S0%)jNn^m^K5G;Yit12U3OBQ@uK|k5!LJl!miC%tGLg^-S#cQz?t7Z z;Be|~%C7>VdQUVZ?XL+xDMZu@L{!P{7&~(6wU;Yz0TH&H1aXY z2zmqGjcWvr49j85IV}hdeI~7MPI8@(sYA9E5rOFP;%o)UrNit-UL6~hGpjoy{mAaq`$5CtV{7izH~&n#mDzU+IK}mX zRVl{_Bztp?@sgkUvQyHl^)du`*%=l~n3*o+g8ryWUW-2~Q~2|Y?t~??G5L8!?JD*` z{2`uxhU6_p1v@%j)GMnV_m$=J^<5^x&F0tXb4N7|ZRziHec?u$XZ6gE#@9>rXD9VG zqg%!i@Vupa)+X{PShsmv^8;;)ti7xKcZnNO{IS+J{nt7F`c~{BcA$aDu_Bz*w(!U) z%U3mA34c4_N1%UfCYC$%>$%eBq%N4fFMa0@eE|!!oFa@x#$QY}N0W$Y(Bme@{5Q@2 zjN6oVzpdt!$Q;Pbg%}+BVGI`{zI|~$D4FFTbHMy3=vs9DHjD-PWD<38sF7ofv&7zR znsMvI<)x5d6%fR&y50`=ZHPL)*MyJ<<&|Z{)N(Ey%>9qhZUmsl?A&i|UVi_Vl>j?- z8v%fJC0XhYjl%MMxS9%MgJj{P5d@VLe42|JCpXc5T%vn0U%bKQCY##O#|%pvtQQdz zOW7jVq=ooe5$iC}_1lnL`17=#%apt&k1HKZsYs+L`(V4}+c^OnRzznVF&*~t>-G_1 zU~_}YWVLKt1LT<1DqGV3DPgc#q93%8Oe67 zk=>~;XV=zhzG!&RPdgcyc~i`+A*kEN<>I>InJ!S6wT?BEENo5!WT#_UD^gltP362i zWO7qsQPr7+C1Hd~R31E;?Ihk(i|GDZ$%dw*Gt)Z!djBKo+~-#BHjcrC9HyV`Z7lA{ z6U>;>i!hXW8KFxsrjb zvgXq>Ly_!UBa!WUgf}J4H**zvowpABXO!R*vc~_i0^^!JdN>k4KR;NRnnUVwo(=6J zq<+b2^DoB0_>qwK+lsv6GSlPN|L7n9mZrka9VYsp)28U-PoK z$ewf-RN2uLwG42LdHWcLNMedrWqICpsuRddp}RSp?|LW&bIJH4g-?Cyx_kYI7JO;) z&waF24ngPoTGlqVHGN8sl35SUb2&Pr!V6RTGvlE4;;dkDjb7A4{hay2%MU#I(6oJj zpf5MD}4cE+irJTi$TXNQC&QvH1KpD$9BYM__MQw|IT}J@>P~k>VkZH1+m5?OwFtsE4;MRJ!z_A)kTdh_-hXe{PZIida5*TBaKr=Vvu^|6UM4WGPyT9Hn)DD6 z6XD7gKXbW`p38amHH}1m+bdVe7aLvrBw+F9bUk{N<=fwU&uzOV0511lGG;F2x0A5h zuq1o6?fG*a4SrM)tUkKA7_{ zAiSY2t^kws@=+}0#1YGWm&ssyBC7=)dm(`EScdb1x4RqD(cL=tk0m84ALTEY3I8!7 z9oBq#t;dl8cxapcs5$2WC@C!Yxff)Y7{7}L-#*V8LTkIsyfjyATmWF(wo7oOdiVzi zYw0Cpc{Vbr`4!r}qI&XU<|hXxg9`Io(>8X73FVUk%KTk}%92tQ<>h+NqtN01r6Y-V z=d<4Y+Tuw8Rv6ia&mS`~vvcblCHI$s@h0?9@B0{n^ z(Kr67SWW4RaDv~b`+C9g?K_>G^X&;RVEv_fPo(mVRM3pl#e8 z-#|c(>BEi0>%;RglgCI8u2tNwM-)f<-$!oih3bp@U@RvIC;!v#8%wHCZ$u{hgH)OG zb@dmOVj|grbJEvk@W^sI-^&5E$;@(?Z}w{+pMcGin=g&~{dK-Iro7Y}w_;%bSs0E4 z!Vzdi%|2Ig<>dedUpO!APeBzC(eY)D!Z`32ErWl4r<++#J5CFLCBX&F{+?Ju?OYHCD#-q1!o$ zI!`TRUo^i~5J6JHJ*?!AR$s?+v_7q`Pk{dvz*}#*HDpE`j5!#%<}J3@j26>6{7~am z%!a8ajLX&t3?@~iXx`NS4hjo^i2qo|E7veiG{lw_|?~$5=O>7c&i< zBr1RGXB121S(!T9=K|G~#MoxAke5l__XX(HaKp9MHw)4{P$UUNjEat0DysQ zL-^MorZXmdrq7UbChFW~q|QlffCc6m@g{0aM9ltvtHMmEmVl$Vn3o*QWMTP!QqW`F zRyOZ{O^P193Bb<`I1OFS77>lLGvY}5nK4Er!5FX@?f|DDvfi=Jui@p`n|3a|l?AiJ zg|!I4tYc=Dj~fgWf@?r(OZhw%bx2{J4AF(Q6y@a~y?ij0fH%d`={^6Zjv|>HV+WCy zWyrBCcNM|lVb2u4t9TJ#WO3?XY!4HI&BDS**`GiYl5r>#{-qx{=5vmBN<<^W#b_J# z+Sm(*8FkA!*`!RuYY)!ixGu<5OD>|M|FJ-8@|K=-R=(QfVLl_yw-A^ak!-PEaMD!< zd8aK-<`t*qmqn8L;f@C2^!MiMt3Fe>E8RTV_ZZrvDVhr`DKkbWZ@l|DR&NND<+l_6 zrfkA=W{*nvLPHWV?qJ;0J`Dwt#sVkCcp03JGHvaTXu@#hr-O+)Zjz{L}|%5=m9?0 z38WJyJe2Xnwb{Z$_~6pjqy5Z4a?Yffa&k9ITFI|fnk9$4dVG8=C@#)C!DPdqU;*O4 zAfyP)3_zLvMXjXYRmJ%KQFY3d_K6HG)umsAVgybA_YLfom%d!JI4w{$ri{DcMO&W) zhz$wl2g3O(Gn8ZK8RDh>NsNgsQ9vxXjvK$yjxhG%BL4$lkHRv7oM-3SDq+VrPL84e ziafC-;PEfjd$_zhwEi1?ZlfJHE4V5!Cu-#G0%1$)glAQ_WQOC=fH`I?beQdC^d z^~hQTLSR|h;Q3}+?P(iNf}`E!L(UZI#F+ahS-*zf(b=;n>Ne9yG}7mgZ3HjGGaB(G z>>=(*LWn?1q_QwhZ(h!e+t{dWK1m6`@#>h=aQ0EJsS@_%f!|kp^d5dYfr17wd{v%&x_2tM8zI?;r74mbrx|M%( z0@o1&YGgP=GMdOrB(tq$R>qoqGOjY>RCuyjM)s^RhNwH8M8l2t1HSUCP_DxH*j;9< zk|B9;zcczk=ttebh;G63e)4*!l8kiKkT^gc|AyK?KZbqdbS>w$PxRJ_`EO@2L%ao) z7MO^Wp}re>daUfH%r(#D;HhfG;2EFDkLJXgSA4Z<_6GHcLxbq|z=d$BMz+PE2D z*45e??8!w_b{LO>4NZx)HK;=Ar$MY_jb<5L!*GyKL_JpBJ-h}h&1gnrQ;>oV zM^sGAw2Dp@fi1^CWMH6)Q|0YB;#Eig)Jj{MpHEMA#tTF-(qoxIkEeb;{{qlQ&!eUO z1e@5GmlNIZJ>=OI9+pWMy0wSYi%aO%AYSGzJqqRt7LJt(oJ*F`ov@UK_Q0pSpWBA>WJ091fj~dNrdAKmI2%F zg=jH%@fmw~LisDKz^~3A=0qkl4Qre{yyt45-z~Zc<&y6KkeX!4V2Mn|%})Z#@sSn) zu-`hjia=W6WSDrfSe&2nvOVZBMdbe`4wWnAl(P0uH8sudzCKY?AYU~!3SI{`Dwr*( z(ts6b7;RqeOK|J#Ie0R0t+4*ZxOyn7NgSYBnwy(xC)|P4LGH$Ul{WGsXaC6vdgID* zLBq!yLtqUVb>UqLVj4V}v@*dUDR7dbkjIT1b^0V{az4C%Q1G-^9vc5lWjh+qi3YVtGm|nsq2g}2_o#!ZfVBi_))!$ zq{P+FrUEteXykjNJ4_h%{PrEDp;?{UpIRF0(!d^%?ZqH!n|Pw3`^qzSZtn?YC!_?4 zB3GMbOmYR`ujtL@&bL$hy6T@yo~DpBewaBto+j4U-!3d3+5at0id2Pe@;O{%8``|N zsNZ!-=B>{FO+l~wkBoam!u%0!=hY#Q611JT!3z{r8i;O=eN zZy#B@SCcS~S3cFS7k?Ao!D#se#q5V*Xz)gTtUz+ZacqS~b%tAj$FPrxq`#Ps&h*>J za$DO5bKY-fdll7!zE{{(k$9*V21?JaatDW_Xp=_93$NhQRvzLH71Uj`n-v z44T&qj!nNj$9)9c9P@E1i(pR|<;Ze;bMlF?BN_6eKd?ENFmNQjnqb^)uGHfs;cd{{ zoz;$yGiyP&>QA#?76?0wJp>xSLU`pLwxhnc}G?;JQ`yhERlVskP-nF^dJ)l z0MPCMe*LDCCE^MIiuI9;Ok$FPzmXt>%k(^z*bV4F8-c*CVmo zkZ9_eX93u&Z4nS!7qnU1Dv1~y9c7i2dNV5^45CAH(w>T1sOO)4y1Xb%9jl{UnfFkQ z+J0711AEa3k7x9gp~Jw?9^CjWyucXc(xL~jKjb+7SJ(jBAmX!wFEWH4ZM}} z8zA-jVe+tLQfpj2a3al`X>f*HrA$vAA4$fr0K2}v7FiPKqE5_}dy%t!dP3XTpu`HM zPD-o*@KU;;3FIAiCGu5QaW`^?rR()WoMTw)(STKKOag*Qz)~ZMQ6~)@W^xhOrd_#i z`RDPefs67AODb{7Wq39U_kUFpf}Uf~pjRTSYrv#oAoKC{hsRlVMNm>P%dOs8l$Lkk z;Hj&9rJN(=xgTBe;<-H;W)f~dKKaUUmsS^+L%AFZqv;c@RpjOC9RGK_kPlHf^GEil zW_|~+8tKNTwGKPpctu1_@NH9Wyud?+qE=@;t-=da_kND(2}8YOi&^Y-dAq_Je`_Yq#xYfCqaVoGrnhawt}Ou5|R+9?YJY`*_oSkc;Cx zef{nHA#l6ZiGQ(>ORx+xmHJ=1psBM>uWg#b=GC=#*bEZQsv49Lu13qGPtvoqPOQ3M6d*_li4Ol5FE);K0tnXS@x=Q%;9oK$QDJ&MK2Qo`SvQJdLac4mWE z({3_`u7K?R$ud?!BJVqJ1Ryqcf=BV zt`_dl6Uw{1#uByLLkHYs#oX)}W1OXP8vGooyO6OlW#GL~O)qKpMW9z&c?tA+8jTtf z6)w(-1V0KsUbF=hVEUxvGjZFdubB!6ioT3taB_0iSRQw;TXu8tk-)nF0p2i9=6WED zUT``M4*|FTupuB2MgM^?}26>t<@H`!l6GW zV#AmEw<0->(zO5-Mcx6&+`?YNU=0CfTp?|2yk%XPB0I$&4b5lMU9R?To0}l6MdTt* zkAeyi+EXY=Lmj~m{-epZMfz@6)!}J48V?P1;fJ3TjC%O1WM0QuIXG|cJ~d{ZOa3C{ zYlujgxLx>N&uzY)y4~O(VnhcCyG{KvB6&n79_+NmWnraL+GD58&nscO_G46Pd{m(; z_++fw5R4z`x$o>A@kchFRM`Thq9!HqvyOQ0TiyrJmR!|x+GeIdjM%jKC$**`t$wZobb8e`jAKSdW7c*}6DZJ;}5Xwbx?j=7vG7Xt^8{r&Z zIMgD4ZcmO@Ukp^#P)0L17X2L8*K~W#5t<>IF-DCWcYEmauqS)1dvXc{r!y36Nft#| z$Td!1e!@)gJNIma)(+OCS}Ptx)_mlGdC-f>C}Ow$$@VA7!f8;~huGcUR-lcB(w>~w zD-#T0`-v_p$-&IXc%uDlH>Pqd$-H(~8*RVq8LD@qWzC{*u?!L-4Uy{0wJz6ws9td7 zXFobtn4yBD6ty0s=~+y{qWIDUo}LCE;gxm>>e;JkkCpb;r(ozQPk)}Pb*8$AiYtC& zfPs^kASfuz6o=v~ZOaNtC*ess;E`A zCx)N4{bbC3dqyi~meZXC7T1w)f79Cr?L6Lb4JG_Yd{_2U7ah(2?Y?v`_4`hzKvqg~ z6HOIh8}ttg403^N91k4{eh6fpGMYq`RMbE?rC&Wyz<6Kx>wl-q?5uDty*%6O zrx(8peFtZl)m`h?*B){JZUt;VXTr&6Mj+*7wtJBe;Nbmq(!50v|C#{&`ltVr{+LQ= zra4R#6+sS8*d%%GI$ukk;a-!?;5En41l{C_z}HL{oaWzICG_TV#th8^+!R)H9-DwB z(X_HTB_;VAdHjM?$b-vpV)dUBlLz=?%yCyR2iIo+gZYLNdA!joSt6K}m9NL-V4a;Y z1(lVtrtjVTRol4#R&#pUwt62u_^J=-)6rA=YuDlVhL2qjzUi*0J zgLiX_>x*bd41GV6fG)VBW`K|6(6Hy+Ph_2M?OYICrZ}o*>+?>am z%ah+l-ZT{r2$o^QuKj4Jw~5F#Rddnd)i{sjJ0l|`={j|JNn>0}L0)}(8&xb`Jp%(B zi`(6jnzNb3^trdZ#d0(vw`0{|EQ~RP_1k8P_q$3&@k(sI;qQr!SJ(WIR=+Pqi->== z7~H_0SwQMo3lUH|LC&44xAdK>G1wxCXo!nKfeI8JI=LXm`sX48nk;-DyYCO5%Vt8U zWQpWZgB0MRnzotXRcqnMOYhle+OiI1BnaDul1?69TdMQ!oNjiMQ0C?&VIwXu0@pi< z?&tKpY?WG5lXk)D#dQ4Vnmk@WOLh6N(PjVMtnvUsVewsQ?T*@YI<=cRG}4Pp|P#3TTmV<`0TW%7Uf+B zbzcvjIA-AnDrM6c0~M|ir!A925WCVC=XaQuh<9)>voMR*N=xQ3Xbq;UdIwk$bDT=L7xw&(lhupne+@V3zL|%{lMez#*_p?+2jMh zB!1@Tju9dBiB#?ckrGf$$Y)nBPUXHgR%a&;`oDbFv|MArerlA+l0Xv}0&w+P=D>J> zWZSrLaX=(~;_H2xP|zn0q7E=!>`3W9#vOgQC|HEB|D&-2F225!>Dyq|^d?8;Brx}V zwaH9odUzkoXgMPrS~b70(B{=uI`AfY@X#Hbu7kgLExROJie52gNlA&#%Es&%|1ZzJ z+1cj>BM~!o&uT>&b}vE0MnK8U^n-<>U&oM<6H#R4du0bUF+l0NHIN$h{_t*y03B0k zZ2Vzxu2IV7X>hyD=n^%;&TL+U zd$kFCcj7Om%om?RBW@c#cH&xAaOrB_7b4%cX?a-{x0Y5QY?kD6&b7SlxuL{n7XQuW z6HXfQC$p|MXyH=r8*0}TQew{4UuKp1B|JIiw}a+csV3AnK2#ZfjkHlOW~?f5l*Qi6 zE3M$X=~gOBgTBhG7FsG<^1DXU`LlfxywXu}g?3Ux<2JE44jwajUU?TeIkKuzI~z z#G=n0&n{wX%dPvc{F`Kh*Hl@9SeJkGmqfg)oyPM7JI%#an{ZiE#N9=B@TpD;BPcD@ zUzL`(3U`cQJ(u^w_D?G~GWW`qGmk2+W1UzD{<%AVM)H}W{A&3#@#nx9T^Tb8@y|70 zx%G6+TkOkgg1}YX+|{ymv^s{RTh3Fmh)Hf zwH=axDM^YiI2NcMk7k~ zu{yRAQQW5WWX_3!3B5%7%wx=o=D@UDlm_^Vyp>XSX#B&VVD zq|~n@(yai;0Wzm&As{T=cNzG{sH>ooY1Wb_E2WqfH&)PlS zICF=-q3vE&dEK6(Eu@nJIyIza|72Xat2~Ugv|0_O?ptiPgwn(rxOJ6Gebktr6tJ1Q zJvQmFNfDNHh_7vK-Vt&|ZFuZW$m1t2b;EU?{_^OxEi~)3f_g! zp%)eTG#2-}KA%e`)3H!hw0}l9J1xXF+8|Jlw(Mn60_8&+oRj#pKg+VbzDKe}QSW?$ zf+;17_sS5rK0TR3Eu1<#=u{_|i#EYO#buHy)BgRG2U4vkn_#>o8F;Z_9nIt&-@Qf{ z8fF&+YI46SDh2Ec*w(LOy}fd%qkX_U+7N-f=KR_Xb_#Vk;z4qSg zfR_cVq#2~_{4b9F;_402jl1t`9)Z;u_)mm>kK{^}U&+8vic)2}73n1Nk_PwACtqQ! zp~6*I%)y?qN+d=~Zi8-dHtr=udp83nirP=LNn5;S1pZbjkA-=gvz?Q2Cc%*~10 z4%0JMx)_y}RqVg0y|j^!wVS0la(33A*uF?58>$x|LBGe(-}W91k_?jt(4@LF{?55| z>32B3UsD;uE-C*&W`k^|-qQBf28P8h6Mk_SYph{wTKQhcQP@Ix`P0LedQ}WsZ@M_- zFcwp;19u;fepi0;Z)z`rIwHra-S-}S<(zSW0n^H3zZE(+6H|BY-mB2ur3w#Y@XD$k zRa}oQwgU|~-n8h-fRzFmQ-d5Uu+PxCV4)-yb2fLi4fd@2!(3eVpKjSf*wFM?ceSpv zjmYOcnD3oPk?|tUy!&(^R2g&Ef!sCryKY5EF%sQmPc(9!a~z6J|Sa1J*ks)H9{M) zv9e~Tn(3tf^H`Ce4T2(@p3=|lsJ-T4++GU_*CkRck12$p`j|x@`^L#z02CJx`wrNR z1z$eiIp26cKIBdYVJ5KoD?=pO+f?ML4D-!d@v8-vg)pA!EDw)LVm;1NutoL%^ft#2 zg{IuH7?TPd`O~Q`ORz<;soRcR$-b1-(J#O|&O5#pF!BkhAX1&pqgn_@z*c-|ja2My z_6%T}47qF@W~@t{%#urU0uZZD$7lji_tfHz9;*ju6ltf7nKIH%kv%WATy?Awpqwqje4`DtgiPY1y3Q&wkkq) zr&6o|L6(zF@ty|I&hQr$(0-=97Sv?=J^u>~q;OZ6V6vnOQD}b={|T8Yfm~|zZckFd z23?(g;x7B&kr_htf~4~?1~oP2xMD2zmA2J?>tANhrE^3COI8wH)w*uP;o;#R4#=wc z#k8u}5j!D$CT<&DuZSF*`B&A}l~a#j>UxwY*sa~%%p~q4M#{J*&Vf!J$9z*$Wz!1? zy7pZ2BG0!t0YL#%A&O~TCfUY<5x1b5yTpGdm-FEI$SfY-mF0%%I_TP;@$_Aa-W7Ca z2T3Omk^%S!Y@fh~T8-%iruN8qEsTk1L9crT+Zsc}_cS}aj_@7hLX>evU4|9;%D>&j zbtZcQO%prSuhuu66COgud6x`=QmcPdwau#H#P`q_1nXnKePDB6JUEJ!(I=?iRAl|% zSs3GITh;Nb_u}6_y#CEeqc?H zVaZ=DIbn@VD~`VRXBgAV=qsLI66AdrWo>s$7YAz%Ic?igVG-A(_V!g-HQzAV zPh4d176O<_>xb|rw@iLFX}#(UUY$_eXE?DAsq{!uoH>~E`C0zE3{F7~coMVFuZe^Nl@(nyf(ZK5@ND$8; za}W=myQ2pTf=1mq6xxdMeQtbsiddfJi2A=QKio=zLQp>LJnNm9!Fg@cY1w4t?!v{O zs=e4)M9PdUe?KH@HMM0xBHOdO%c`CuvIgKvMY;WkD}P+Pe0#t|&?M-V$dG?kn_LqmqdM$gaVjQIaN7eVTwi7Qt{{o zRm&i7uH@N%O1Fw5a#_X--D{O;6UTChp$|TQWYwbGztOR`uu^xRjYjQvjz^kV#h{p< z+4PP*rkXu>{c7z2&7D7y)cf0w|vEN_6cv!(6y<22VFN&|K6Uh|J-(Way~8{qcHJ@ zCN;>ZJuDtx#cAjgTghMlK39wtDX6liH-UWhZsPy9=H0b1?k%2jtOaE8;cH2?mHI!n zA;o510vj6>Vq5pP;;T3RAPcAXu;V(tDw|AQ7Tn?3-*0)8Y|1Hw{1q&o$2SP%eB>fo zSJau$qcof!x)u$av~GjfEISIV$0Zp?7U1`thOjSd>a4T%*HEn@A)APG@$Yc&Tq^V? zwU%<}^wBDv)JMfaSz>eP3zHVtMtY~-$*wi5aX4);pSk`{HbF#Zr8AR zw@eJg+^|0#7EMNV`kV`ux)RFTBZ~7q1n5*^)yYqsTi+2B6%{-WAkSiB`h4bp?mZLp zxs@-n)t2j9lh!HWgI%yTTMSg0D;d_k8}YPNTVp1Ej8b0oZ*;lSW^6L0w2+Bzt1?z! zNb9T5>zFIo5x0H;3hCBB&m6|JdUJxwzUlkY_)mLYxe{ltp;!W1pMa$Zb)8|=6l}<^ z&Z=q5MrgvUTHKVEJozKJB;NG?CE8m&x$Nw0kbBf~jyM&{QE5BS$beSvTw=7}oG|oq zt!FuR71%zP#M2@o&d5+F!;xPVz?QcF(iAjjeZ}8r4njTkRvmbrSeDGO< z3D45~nBi-yKem+~UlS7WVbBvT#m&!c)U9so@znX@dDr6xP1|-a0kbH3 zEj%R)7X8t%$i?W>zOBeg(f{Xy)F@pAfD#GYTc!2(hYU>?NpzZKi>P$J4Kw zEXw*HjhfzT%?i#fcsuYV%nXyAYes09mQl+`B-m*zPA)#Ju*2r_OtmQM=HvAYo#in2 zV5hw*FZl19zv>TNYksZ*2+aW_ydO-iq6mHX|&cFV=TUQ=obwMDa9^INjd3Z2+$b8%sM|^nrd;A!tE*3m!o!d2pL1T0RU^s$ZcZbX(kE@VG zG2cOq*FOb2zMSq_PqcuYr3>XM8b_48v>!@FK`Nbmlr*GVj~%91hnC|ULU1^zVhxns z7;&s8(l#n`%Y~K$=&xM_#J)~75_bgkTZ;T$ZsWHh7DbHez)IU%Pt)TX3&*R=!@5C? zy<>a4;{A=%n>}0&U6LDM6KaTlm956YSShBjFt39(N8l$`clw2X-1oK*_uuJt@aoau zNDco|7+2Ha(^WV(n2?>Fz814^6DSg=-jEeDqs$l0AA(r18V=8m{>eXg)@Puun!`jz zs*)6pduh?j((C0O1UI5oOqtA#bj*ZO=5P3Pj(P9^4E;t6f_DF2Plomnj2R;W#85&_ zKIr1&62U3eTW*xr1UZYtBki|5B+dn65&niUE$>O(-oVGo)3!Am_!7LZ)*}{9u{Qe1 z8*0EIC0dI8`yU6io9DnA;wCuM_&jwA&)&d+E%v+?=6W9jEeB=*dUhGWgzB1;8ApE60Jke)KMac+6o@G5H)J2YjfN#t`Gv8Q9#ZK9V$pocOl2q}|v zdJk7H8;fxfQXWu;#&kfd3y@hyo}{JL6Ia8y>*H_Yujz@K;HP#>uHu9B5KlD!vMxvA zw9?-=poUv%2QtXu=CovJat`#5BO^5)nUdX>Jiw*p=IsqXoI(KzbRe3Z_grvz>jN7C zxNOWq6oLl3k>zm?9e9#g+xVJ`ist6h(o(V&E0{%dJHk)Sw(Ql^)qgv9sgwYBh*^UH zvbiEl4Dw+M$U+*}?_S=u2VRqiV*}VBc)QFI^Os7tBJv&es{0t^t*o74Xws2})0eux zw0~9Q$??fzWoo47!;{0DDw@e;QZiiOxal&bd!)g2V$U_l1v3scc zv^y%Sl=1!hx0$pT(eyMEgp$2r`f+no;Le%*<7Rb0(Bxr{Q0(81 zXb|GP$w{n9z^L=%*?holg~>WX?E1QiQL#4`H}|~yVRq%5)Ff!s`Qhx@1OMYf*XHZT zJ7u|zh%sj7fPW(xl;3_d2e|zNMEHpu6Boa?8{wqt0z%|dG-&OQJ>;b7xB{ZNE?1i& zr6ng)!=rrXTf|lky+UZCj^EWn8;BR}OEa@)?{c^%v7_)Evr@NT)H=v6Th^;8>K_fy zJRjSlf*&GRVY_bTzkBvhKrOsPqKrhk&#S4B8x3|0-d=13g@nv`$O|Z=;BfnT&xtNK zcHa2i?tZCcjkBL6FeMdj_h9caw@w*h&Q7;eO45l-mU08VL`A7-6$#-~OL%B*Ur z<&ew>H0OxtG%0g(3FGfBm@SrQ>0h%MpatQrdnF-SVW$C_=Z?B;XY5uY6nK%6%s~`y zL`5ulzozK$DOM-$-`beO5GH^lV=oK9H7Vfvv=SXk!HA6%GO8FaOdcH%k! z6)t;%buP zhv5RBa<@>W3HMAi7ZK(EL@t$1hokML``YJzuV@u_Bbo&h71bOBP2RKfvl&>NhK0-- zl-HWS%`)SlaxDKSKw(yC)v33->cSobw=dg(m~(m6Xf@j~gH! z(rd#5Dx~RMkBJXx=b-5F8SI~-^T(&l*DH0)PUML%DXD4dfV85u6B#Wx;Ea3Gm8BNq zN*eIZ>8-<@^1@{%iHa%#It#{Vtxx?E0Aw^$CSBRTl+pX^>*hlhPo~c7;LZAY_jnb& zow)K3Ye=)X^Kob9`G$sr*zzn3^O9QC@55nAyBKqM?JZup7+uXmh>jdQomW*dx#XRd z-0a`_VrZq?8GTWu*DqByV<#IGq)`GHPUAcvlpOqZ;KhbtY7_2BQ7$y3;n_>PFx1jH z;A&r~4Q(fppI=oP`s?$2u_@wn?=!0Q$#uEO-^dj&?!rTAM^B`8a8aN#++Qy>gFcZO z$fbU1uKq&J_yY%#ot-EsK*5z0nQcPt66D)m(&aNKPb0pOs=l%@wi5Jn<==AkEh6~z z!^2kaKmLE?E038g53{5JCu@QL*73mC1?~@LyRXGIP3}vACxRbxAKAbwLD2Zh!=dvH zybHmR`>>AkaC98pxAL&;e6!l8oD=NQGqM}8>i~%6l#~gAVuBw#j6ovqB9TZIELkAC zy!rem-Bqqjkz88!#d&saN7PRJ!fL9)*&2N0wl#lOZ6e%WoSBdK1xo8we>NYV+9mQB zHxC@*Gzx)#ADhcIrsVnhyv4scANCCTo0-kaCkOh;l}ZGBvcvbIj=e>RjY3PP`Ca~LkSiKv{j-Nrxzqda@9{?!=F?8)^z$m2 zRtnj-aqD%)X?NFbRXgFy*5v5nS4O=8<^f!5s|R0tEERqyx&(x_eZW|h^RmH7a%?os zD;uDM^AcznrOj>#Olii{+G-fg?6WiR6io>OC#!bjF-iivC1SDIUql8mo|^z+h0_KF6SJKzi zC05m`HL7O1X)4DG$mN1`TkXE3tO*i%tpae_U$}~3ZT0Hi*^uQ zK|A@w3&ggS#4dep-p;$1eug5xQkp~uwGdiNOei2=OSLY8oSedXQu(k;nz>kTcTdW3 zqfDpAW^uUGP3OuX@|Gvq5NcwCcvD980GoLaQtVdB<963Q|384)Vn`?`+astN*w-Ur zQevBCFRFa1S=I#5i%4ehIbLA1dEUrcmK9fARnxdn&>$+>n{m<@%y z+uODE5$?JIYK$QZ&RnX~hbVQQ#{s4Z@qkhJT*Z9E*?XGPjIMj8;CueA^?|5~o6Wjw z->W}@;M^eA^)SBu@9MSq_Up$zW$+TwwR!wFU~)QO67U1OIGn<}AQQK%1HjjqKt_Z8 zMFkNzh*eiWC(o?C-fxk?Ngi(z)}3G3gc{n5%WhRO2ox9O8R`j`QkxT(>A)DaW)>7r zBhcRFr>b9c)2Uva@m-JocKJ9+UWYG^8Ra7e7WiQSLE%E%XS)0TqdZ%y|50?_;Z(kH z98R|E?2Z+3WQ%MPI$6mc*?VX2kr8riIz*YF>`jinN3tC{_TF1IzvuVA>*8{q_dU<^ z{eC|8ZPPwI&7ZLk7}emyp3nU&s6GcO?BMl4KYa{{I4f}K^6Xqc$&G;hzBXM&SmCLKP$(232{SJVEt7DtPgTjcfssYFMI`G)-re-J_F+@fpmg%Qt|2o%XS)R)SaaOI+y# z8r5#l&`X6Ve@euwZhsK)bSz6E!P>9XUlM!g`6DV4C1AH2lhVcAwaGsA5*CEnLr?&2 z-N?tZ`ANEpUm(Z6B&-b8O&-oKuRT1@78)1_qNEBG0DPvXkn;jPO}W)T#bAsyt~cVhDD zW1a_>&usi>Y$KW|Hk<>@OqrHA^(f#(8g*x1xcD>V9q6gpG7tO3Bd;}fAMFlPL)2%$ zZRNk36-Q!LuamR$yO4X8-(wNs$SNh+YIr=mSMTH1W!Kc|IXBcm;S#ESf3Nm+Vc2Mn z`h;10j-wr59XPTIqN_iLDR5?kFV7fTItf%4h&xPAQfleFUE|Q5X5T&2bPc8AwS=%i z*^GYY?_QgHd#e4a!d1ndZ7HK67Xx2*D!VI^QEMPr&Q`TCVD{G4Sl6;=Q_*Xh{^^Fv zhApDM#g9i`O+EQD%@{Xu(@>Q_{D;kweC`6t;9H4|hQdvpSDBNp9^F@pl%S8x;9@ z6!hdkHihZv^(9${Y21%>H*bM8@YG+NCsBX>%D|sN3_7Z2%1YYbt7{)`;`$#=M_gOq zq3@E7Z`K}2U9MmT9u83ZA8u3oU!<#^Z@1hCWd`1c{8;A`-8k$k7&TKNwJGfKv)aVK&suV7_r} zZ!s)P%Ugh%p7zO8Uc43xBxqr?XfzW?l_{CiNkuOpq2cwiYFT0YiM8co9a{_}P zW<6X^28#g0ah%c=GRk-|z4N>=;41uouTE8!(w{fHBmK)0MZz0_cgtlC)SZRyDK3+} zpIU4mRWOT0ow~|vX`bX}$o)wsr+6m{kek&dV6L7R_>v~f!WP_hS0fmk*H4R5z(xS% z%galAG|jV_B4q(#9mZZD=K?J^=YDa&hf?Ni5#{uow-)`whZgpTW9)n~f!nz$eJwM3 z5M}MI6`3@HC}|-=64ayI>i{bd#LN%)j0G}>@I&KkOF@8}m6(NbzsS{=C^6o zS-a!QQ;UdSnYh7(Utwn~FNpGFuaB4wXJ85Ef-+NLV5W!yh>qs*6A9O$C@h(~`aO{C zbL^21ww2v8;qZsuR;;A<=#6&p}x7V?8RzqH1>d zx$DDfuw7_&clfaGdnCyjQCs+PAj%yHCJPmM+#g-|1KRDG$?1nm+Jn!xLi0D7;u5c(WHcfeN$vYLL{#N|R z`!kD1bkqDB4l*Xww$13N?p}-@7f9cmiBWz-}XJ!mSYe1xgoP9;F`+70hD3NdDjIY*`KPYQn39M-W zZes7{4=?1bx!270>G?nO`;Ajytl$sm&+0p)6w>6`Z!^|GrA#1w5d7c4+xAF;9q+zX z6|G}Lx6)X0Qj$4Xq}D2#`W&4kHVlw!#ddiA-)aF`=x6?_%c8_8dd@d{+?&2^_dn-n zbA)+tn$doFT%FGr#S(3cFH2iur!zK%m|i4Yt_Bi;o1lS~tDc~&S|L3{GD3r4^#d-V zA8EII-KX4t>&}CtpLO&yZ`r>ZOPyYf?C4--FL?Nd_lbnxUGzGm!NGN?lGN=4u)hs6aQhQvuP%N(vE>bmeq7!Re!5ein@b`O!)08a%)rZ#yAA2vNBLC@mQC3e98K8+J6S!`UCe>^ z=5G&hpCd&hdd%ni0)y8(vi6VXa8)DC!I*G*|3`h>(!!4^dI6 zg%yiedQSZLoev-eL*(IU5fP7Wuh-|FWzHw$zk3r)!Y>H5gr-ODI>j*BbSE(@>cz&9 z=>BGGC9qfn|JIA^U{(TW$bfH6PAO-G6w*R)PpNppLe7x-`zFHZap6hMj~|8MNHq4{ zd0f!O1Qfd3|$NXc{THB z?$}zw$MsWJA~l=z^*JEGDI{XF+^x?i?GhO15E$w`hZYV0E$Fw8+AXa*pdWqCjaC7A zcM3gugw=N>#2IfUo-cA(QDcQI)`KP%2lD&LxC+~~X^Czr0=#5!P{&}>u(Gm2F($@E zZQp$L&+p`z`fgJvkQ83<6FsYoMTvS&q@wk}2Fafopr^E^PlHw6Jt{`*qeU*y9E)h| znXu|Y7S&xJET3^vKk%tY1O-)0o=l0wgAHMR<=RBbdgD6xIg`r2B> zurhd;*R+$JZj9R7EELgE7_*y%wg0pId#@c0P16vq$3Mpsy?Eyn9cy0>p@>ThEURto zoiXx;gL~Tgm}5CbKftVE(Q*0Xwb_E#5CJy?lGyruM zk0@OTRhyT>n&av5fk#R31hW=SIS?TV zv}!Yk^NRY4=6oJ8nBUWh#<}0r`Dr&i^khoVH-!L+2U})kmGyVaSn7|0-K1UkMW^9~ z4nK(maiU#as;yF}ZNR}H9SnVkgJF7~6SwBte#)YH#)XK2IqDX6oqxreg^O+CsQ&i%f8BP~H_xNc0sG5BAv+Gw_Wt1aP#`f4Gck{N_ zoH&0qXOEAB!+B}g0hQ06jt+YH41NZp%`@^!<@PDVk}tdrKZ+8KcufTuB(0pnO=GYrB<0j|npE1L4m{f2186H|(EgOd8@U#3;lCX~sRZ)UY=dKPf+hyU_-of9R-8o3y{ zyY>UW)vwyMuQF=8NF1_ViT*xgEWLmv3!RIs{Ne0UFVSKmMq4$#!`EG|_ntQHRipn! z8Ze>(a;6|1ez#Uz$1Huux~q%J9~*(C^K-mBA^9&FbwUFZ)vqo$jFs z0bh6N`@@U@*DY6eVB+BI-03aWF~MW|^s7#ukP4eSC9Vu?!h`KDpS8K$S4xGw{0sdb znm4b&_^oWdsY?t<#@ntK8;5wM@1jYh{o6)1`D3+{LBi#NLW_AkXecl5b1EYC>tcg| zwnl%O;J15ul!@WC`hZp(TSKAuG_^iI=jWq-3HI6DPR(~(A&LUzwBxGe)uYwgge@7{ z!5+?(axx$G{Z5h?Iu^4uoPmkcv46pW`*54BqiL_jH1c2+^sbbj1 z85l_I;tIJq;6N8o8-o+6)hmk(@#2JkEh_TTv<%+=^h$WBYVVlf=v!y*?&PC>L0t2^ zDd@+2-;gYOI?rwHeA|YvGAxF*ly3E-QBZ{g+04n>qbIiS`t!lq#&C?TgJ*!_h(H>z zZ(FgII`IGQeGf~lYTPH>fLF_8#N`IOZMvIQCDAhmFc5AHWR2>FomLj{=kGc&zwy$T zkJAu(0~4(<3`_ajksFi1)$)heMofI*7Im|yPEa+h3$~=~wJdFTN6NVkHWI5Mz*yUW zR$tMx_Hcgfm3VLLxq3OsPP!RdjeXfk6n_-H+2~>WRGhW1g5pEuO9eTDG}k~EFqxsS z{A0rhAb^ttlw;G$@G3W|0c;nVY{Q5V=NDsOGGbB>f(Z*gDUc?%Rqpmq zboTY>Jjqfm+b3t^$#n_aephJMv6^IFfxE&GZz*hE#5?4O}OHg*`JBsh|0#wH8|Q@fLmfj5Z}7xa1! z+r%sYR(VFh_s99>9|3OH!~`X(PG`noeyKhRko$Oecsu_rV?$ww%U<(UdXDwY4ec~E zClLUUVE#^N;R?88*Ny?)2qQ{@xA!OnbW9BEigm<`U1*3&rkn_FHyQ7M$wh$nIDOu~ z`(m052E(myeoPpkPY0L>E4QQL7Pg9InOVuQ=1-H0XyRayOa^{hB48+9o}CZ^JO1b4 ztF9fczuIbv9W9}ii4AsmA*mxKC*DDw`mw~sk-7DLBzB%?`r`;T5)u-DLmQ&dVAdP1 zl#rb3ja-udeT_)8nS+uV6=680HeA%lD3VufNViNF3S0J(f>k|qDs>vJ+AM2vCG`Gl zKFlbLRO}TRlkMdx@ z_!&?cLAGZ<=Ij;FVJFq25!Fj#k%^Yinecd^Z~po5-f1d5gxvjvIuD|+AHGc16g<=W zJe^zq3BlBe$?LDIpmMxq`p2m$Si527#Z`SQgf$2H!d8BEd|XAojCyRF4J%kRZPy}h z#gD@0l_7x->#QS3?0D~8A?XC2Y3;jBK?kU4kv6xMbw)(XNNfXRZlw5H5r?v|FE&n&Z*966p zpZ;MN7Z$$ZW5LCq*d35*n8}PPyq?}vxBT}u@}^}MUMYWnyn_!&^p?Ed-c&yQ`Bcqm zU9bDFq8EJ(5J1l=1uelEVG&MkQ-UuM*H%8{AYs*twGmasxBnk*n7`SULd5Y|^C92RWH?M2&w8D_qxmI%GW?c{ua%yJ1gRzAa%hdlJ#t zH}w;6P`fM+=flS*PpO`%M{NItT=lWdh2F5Lx zHAVu)v$@G#Q)%VY0_iF=#C|6B?Y%6UU2-mw+oBRao*adCV+#=sP$jh$hcO~kNC+lq za^>{??Efl49uS&G$z?_QLG4^@X=_&!wZDf4*vT1SGs|m4Dwo!0@p=^gLL7YZ?+UX33E7iQ2v}FrNS5alVLTwAl*ZwSuW7D?fQw@lflt zGLc~lU#zI4c*J!jws(_`Jqh^1yT#EesZ6##pQb5HqSge*iNDCQ7WQpH&TO-~@i*Sss>8 zS5S&zMt(YAXSX^p9kOb&tefky9p4U;IUZwg=7c7;-2`*{EEkZ~m8ZdylW z?5$4wXIL^E`^pQ)t4-b`%T4+o%s-u5AiP^2QsQhV`gvzG8jucH<60(C(FLUl@SerAHq4R&RmX$_FR^A!tH8f+*~_nk)fHJAbSI{|L*gDZf!f8FPb!LUdr{h8N!== zX1BUfeFhPe?l`)+Hvc!o80){&ys_6Zck9OHHA#+zp~x{RXjzBugEo``o~3n#H`77b z(&!|&Z3CF3wCZ=1HD4#PMmg8y{B{)`uLbxsL@kfr@04g9N&k5kg8LGv)t!|vG#VID zN-gf1)4Xsw$+o$vNF*d?64rnHNQ%jG{74r{nYrrOlwC-N(_~6*Z;Abc^cU+*ss-tC z4?{%3GtY}~4LYMiK4W;TnGQK_iJ7iU)xcN*_F?(`hr?@E>r&SQ?2ASTHtWS49$ zpDq1DPcH!Z_j5Z6dBy38q*UN#-IVuk?mnX=ITqI|fR0sUJ&MK*q~Y5+)8kri^gBZo zJ`LNk;5uIY3QdeQED`F#?Xf_H2$${oTr6`Yn*-^z09{rJtBArVP&^QyUnLE-ttbJb@^r1Cb9NY3uR_7aMK zncptyWRhYL>oqT%`t-QgWC=%$ib}2>ynT$Dp3jCSixbLFVq4$f8<7hkw#J40AwHP- z(C~a_Uh-gVmr`X+U57irrWm_}8P>Pl2=D8&<{!)vTTDRg#5(&)oNQ30Y(OHk7$ zBamNF0RC5u?&qVsUeV~npBp6KUy1f-bGcPeYqJ5u;i5NJXHB7e_Qe{waxCALH4Avi z8jfpPp8mLp{c-;zgpu(XI6JiKI^e0Os8I<{zWqYQ8}-Sz$!Wasr^?u3)GIj!ImAGH zb)o&V*RM3gv`Sqbo9Ju-Gr;o9N9{lRqWndr0L%1s#_Bi-yEc7xo;?j8bX94XRk5mX?wO^W;zC?T1%DjH?^VpCvMvh{cJ$Dv%>>BD2_q?MEhkXBkRf6~T=C!P<-7+>>!ujR%e zQxT#6+1tiEyEnD#SxjYrpO>^fX8DDExd(hb(gR5Fip>vh?(rV<9=-gq*!@-;2y$FJ zJh}!aC;>`k))VbP!KOy~p~hR$R39Vn>?!~PGi#>L`>~bSK>igmS$WI}C;0f%k0;%o zvy++k1@PgKx?IfpHJ+cFKdGX1TF4E3AyKX9`6*VLD}}Q#Ki|f}SVuU9b>(#!T1zdH zzv1~mryV9cU3!@|N2-NNb9;}H{p*(d#@Joy#miHr!Fom3P>1kRvQn_iO$dTR6K*G?OtGXe1?$>t=0=l#ynR@;6ZJ z)U*8T;o^tAy{qn@O0!4p1;tj|8B0ye|4Dn2rz@ry|G9~E>Gl?JYW1-uXI#aX|LuLIQcou5{nIxwyP3V|2T7j@YiZ=~?dqy7$Y~N7d>OM%D9cUMU>5 zWF}tskFXxxfbBTI^e* z+^6qJ^*jIw0UCC`O_o)&6-JIC;z*0SL0Xc69Zy=Hgj~s#^&#mONCu?fDv~h}th7dy zF6;lUroT5Dc`k$-0Ru8dQY>bLvLV2&dCG|Z6xi`)ouAWVJrCx+rT=y0&yz zqAjk^J2=;lLE<;c)xauAHawKfW1@0)XT^C=KZl8x*s@{KE`={zmlp*@CkV^x7&g3@ zpfmWfRFmR2<63>`lW2qFNynNbdU&QL^QSz6eWC zYk)@|-KWj2am1CikzZNTeLK12zR~n}NOx!*G+y|!xTo~R;%li0hC9r7KK!ec^WljB zPQs{4F6aPxk#{wJYHeQW%0l9v=Wj-asp98Yr7-eWooXRvD<%tj@DLVk=zyF4*uo`` z)|cbGOZx4JH1Hhcqf;<+S>Ee6ROF8BUMjPE%%0y5O?*7AuSAONnl+M^Z_7~>b?VDT zX5P=X$JW^-XSCmn3*)DWNrn9Wq5J?$MU|$kszZW6=0Qt9fdyu35+dVPq*JPUPUoe$2Zgggy5nin=RyNZ7Di2c4M46eJo z&^>R6b-CluU~m3Y$>k!}Ra|$+5V(iCJN5PI&g(8?irV5|7<*nhy&r0mG|=)3V96}D zVKnb2DpgxP(sv=nAZU zy>k|CS@U%15;Lq+WYjI*dkq&d&H3~)E_5@swxqlkTpHr7ZOLv$!AKwd%`P&P&`G7M_Vi3a&Q0iYo?vWLV%7J%S4WDE@EY$cob0Q*?_N z7)Z(cBpP86?@I@@neZ{4$O3JKyw(zw=1FG;LBJ zMTtD5r&-6D|8uWr7+GKQkpo2tf1me{5c_v9>D z@VDr9I16TZ=-19Na}tKF8YELL3~zUJDs*~FbbAF97Oq`hA_NDW4dX<%_!;6pEi-^G zuuY5Z^7(=kR^XqzKV)4s4H691;e%Ezf+ZafGg|JVK?%$EMmI5N)R6tn=MF6j_-A`T zI?%4F|MknN29c_`OK?>u4RLmLC5)9->vemC!yl`4-64I=yOD9X#Ca&Dx0O%E(x?sj z_VsAaZ=WoT%u=g-u2?N&?fU3zoYZY3+**}}9-^+vt^Hu7^+zl`I^Hu`ozPddJdymZ zwvdW_SQ-h~L-24y2k3g*U4N}z{x6Zo^+YC%C__0veJ5xy&#m#U&me)O>5F_vpX&eq z18sB)G7}W|nRL86i{VI|3t5X=ekEgmwQBIcf zGuqFdA1v!0VTM{v|0<234zCX$D1YW$=g9d{Y9N&tF(W-wq4&N#CYqrfv1t?S2tNB! z0=z%qz@gnG&f2aWRd`S5;Xe!a+3hnBNAb?+F_W^cGFr4-bh^y=`eW1`_JummFn-$0 zF7n{%=BY(zp#dSH7+Nmk9r$4z8K%anz%J8jBS5|wy`57Nu?+36ZG*oA&ae`e*%~!h z$;c@6Wp{mRc#kHz&4XHDqpd~(Phf;G@EPPrZ zjpyU(^T%%}a{)wUn3t+nU4uO-Z%i3s9wG~kr3lL(;mE0;N*3UP>`YhW0hB6NJX5SeM z6*ws#`NPUm4py!?I_AROw*>Y{-|YM zJ+>^!8vJAA+Is-dh&i0Q?#=I^_T}wDQ71wn$iH>HRBztS=_+&WqxS7 z1T~zEE$_x5tSf!c7j+-vY>;y3{y0Ii6uv7`?b_jCTNo}eWEp$6MX5mJCh|?qBFCg8 zD#~aS7|1wwO38=J$e%%CL?on^PIjXEyn9XfzB}4KN5{;ph?hFhoaf*IFT}iA`2OWP;^)LqFJ?Qxr^iLh z2mXxxZ5rM9yr@f^i+PgyK+Z%C9kt6AyTL|gM4jwdl(y>`GbUKC-}nny2vLiB-?REe z?C1MdeS&xYCCD&+RnKGfU>Q2@8BfZDd%NRGEhNA%$Li147 z3GnBER}p^hhpQ{UEug?P8r23lgXF_^=%cm4cee)-(%^<}d<^z}>RO3!-MqYbAO2vH zLuM3f9)%nqD_K=x0%Wlx3uJEAQ#PYOB4D33C1pLj``b;o3x2NDzL7L2=Lf)-s|jI} z`M8)lozy(VMfJ_ira*efKrL!hQ_|)kIl?hc1Tb+x2w7RYaJr2GyMn{Px_NNvBss>m z)jvGVY%!HG4jz%1;hl2y+8Ir@-j{}PsDNYha-8&&DF6P-h087a_hs{HIz+Z#K;x%p za92o+o3pBxj9E_urKvS|IKq4by8NNENW=cw8_ec?H*e?G)0!XF>G`Xt>8NDOF5sHR zTBVT(NA$ni1|H-ZGfi9O0SBerKPKteqT>=6WYF?>U!En|DX9AQ4tBYLAyx$B_T#tE%RKQEb z?(4+V^?gb68vUpbf6nR~8v&Djct0%cCNOi+B&-G@68mM4n3fPcl}&F}XVoc0^=)%R z@}W;bh5~o8h}0rO&8iAu;*g0bh@&NL6JaxHp(!zM5MRA;=|7NLVMwlIq5& z_P%%aXE3&;~x)U|D39J zc>X9Yr?YyHWwYSt(-5BTf5*}WUHdw2i0EW%(dFpkgX;W}hHu`&MkPoZwjOX23EB0W zk|B-SD6F#E@ru2=KebN;9RQJ<6|qgvSS_t35mKzh<@1R%Y@7sEa^V#ZGX1Nv7T)A* z44TvE0-mh7l?enH)P+&maCPPGgdBBVl18F`O> zXoV8DDoJRrEOhZyfrIq7Boyt~bl9{*$g(b2bcoR%wjGnuoJLf>k{K`^Ad@j7#R3@( zH?OPK!x8+y$1aj_l~C~DcAxnlajkF7QlbYDs4}A}w3Q7i4+=&<-X|wSF!Q70*5jA? zRhZqm9QpE2x0dp?Ng@LCXCe|}d!5XQxI1DDxr-RQEiG#i`^Lx4nF4eX5`KZig>AaB zYLpJO;?+gd4qgC?n;{txj*6ipAtnJ2>?yQwt6+&*wk5|?!$f^CNn)^;gwHm2`_(JJp&w2uwwwTPow7R zghHs#18#|Bno_&q(aPMKZ@vHX4h)p=d)vQJn-1+G zhZuniM=MA8mU^HGdTJ&AwbPJ%BD{gB?MEx=_c;q6W+D^Afwt-gSHw3TR_*$VblyYZ zb6JG@~Hu; zV+ja5^&IOQ#=cVFN=FYneb7>OxNocuscg*RALW?V?#?cRhqkL#PyM-Mik{|+{p2UP zbo;@Z&qN?yKvKfQm(Q>IMVQNL2)l)$KVtOt{@-dr;2B7kn3*+u7pfg{f5z{yn9`$= zQQljo)%`ik9((0`XD&x`F+F#-#X#3l_&dNk}?^@Q6khjnqM4-816 zZ%tm1K4>$WpYU#u6AGVo>hVAPu&%&Y;{kb&rXM&}mOJi-h}p;8)G0$BI2hDM~_ zpCC{OtT9plHBAQCo+YM8-`?vck7)K5e22eX^Xk>!a$jsXEWCnS(rRMSc}&Pm#93bI+PRho?DM{{k52Q_>qHaD0@nLK^QXf()+q z-%I~J4Lwl%ZAo}OYufpD?!rgs+p7QljdalyK&^!6pDSKXN*j!oC@EO*gF6f_zd6>{MZ z(#Z55boi*#@#h zZ?%Mi8J%7I`_aN+&`)cdMjj550X+gdWp@4j&H)}BR^LpcS8tu(Xw?DEv7No=bnSM= zW2w7UYw(?YxL$UroR&{x?(LmgrC-_`l}ybT(7N7o%_c%MpVF~DI4HDzkqAG226!PP zJ;4C3)^~_+#FX@T)m0{ey7T|9Zfgk!h4A;BKcBfkTwHx+Xu>2Uxc;B)>vAjkHpLs? z9%)?O&@5Mf{YHPh)P*bO0`3oSfuOU}5H&jp`w-J1d z7ZTfZ-MYIA2#AQnLJoq8_j*iGov}JpY-h*dY9GD(gzdoGw=W*V{BuYE`DSZjBQ-P^ z^GlO>tS(GBG#66XKy_@J@u^{4OPABHDF9nrQR6w~_9-~*!k2AC^XyUp;oK=b(Mt4; z#0@N1oCq`4$Huu!k0RS6nWAlC$W+zF%o>^sWCRXtZU68L$wv!HE^M4fhla|wLrn$K z7Uc1L)Z(cho5YJpu+&%P=T`_&ol3;<5qzPCxcd6wmv%n@`)mXkEbr~#t~mec>~krV zcU5S|PRv4~T1V(n)GClQ`pTlmrzXwx;kJa`JMbzD9j5iMP|WMnRUP7LZ5#d!s@F}P zVdQRSdbEIw-?@! zfT=h;4#Em)z?%{M=)Cg=qv`e@JoHFHLVD$31*?cskK^qpuA$aV@@1s|@AM*010C~i zx}k^fV%L!Z`d)@1e4rvH_n~sS+&k6H%WuH5$qZ?fOPgJJONa=*iHS5&eC+ zapA3>O%JcdL2GJSzA^m#I8osKn=H6zQVz#}U$JH^A7R7IiqQ$C30n!%kaKlt1vj)j z(yOdymbrUEm$17Q%T&&Y!bYc)LXmUh)$yGv26lOEo@4qSy9I^R2HTO+$Ap+ZdxSrn z8n-z_9cmoMRPY;Qb7egFVs*`{Idg<`KJ~wCX1_36Jv$22R9tR&*?8&0JWs8TI(0Vu z6sTUAB*U2kja=c=@}OYUC)4mDSdFsjcp#~d9xaU0*7<6{;Bv8(Ts);#E2>65%y{B` z{Erf(WHo=5;@?D?w*0?0zb$XAMleNrUu+3wFp{!MHs9=B@tEWaHOC?R#Hb3R&v8e+y@uwV?1BxOVEfJCeS8s5MK<&cGOWEp?M^gA>AJft=>w zAgP#4To?G+3%_sQb~@Wr-@z9J!YzP{xOs}Joc|1PUchf(-`T-mX9v*foA7D(?)mFo zV?a9Vb!}oAtnIu>O{xN!?mJ0_HMiTvple3&t2J)$RKVfv5&@UVhwi_3OkO3juE=*M z!5>_=vJqguQGMfmcY#~1^C<8PIiIllGy2I@z}nf%(==WLsQvNq@}|CF=Sl&LnfE{; zK~ANnY0!jRL%$DNnHZ>B`Pp^4Fu)rmL*tX|@ur2*&D%e6n`4iW!Q1N;vAvD{qrr8% zpIT-oroTwonK*wD8bgBFSRQrC_oTJ8E6Vj>*4F;ASBawWcB+sB<#Jend=n?x`<(=gZ7P9tr~zY{h?@<{#eqU;^EnsvYq z?jED{&d_D3&aWw#(P6IcC>eSSx}L^yzo>df%?SvvHV$1d`{_Md!$7?>3K70 ze6%~_FPTV~Hk~v@Tc=eM)na1q!TsQ&9tDzJUtO5G`i7kDUk`9kz@qQ}qgJRv4SzeZ zsl0QH1T4T8;-olI?%2(xcKyJ_e*C7lhIRf^9UppcSR}QuyMpSSumzfE@;R2_>uSy- z&FWk{Nm^cxo;JadR}O&Xo)XEI2a&1x;t+hVppJv_uvoa)ozJH6lAY;qq5JiKcZB%9Q{Zh69*SH4`@9^2* zqY-mVWcM1jhaYv4=S?7f$38J=4(c|c0L#CD{lL5Z-u`I8c{28j8CV*4$dxo6l$~vD zrCr=~EZ^<{D&F<^5u2sn@!FvH?M=%a%mBqgX!idh7*%FmrC;(kb_x%SsBvT~1=(UB zwnITLx{{*W1kwY6{1y?3SDQ5%-oM|>xg@f5w3Ijie;{>!fDK?AFQK}5JH^pLAhHl% z`2+7d6=YG2sIC_?Tk^R$lWQB#ZrR4B@uW`xhs&}`0bdq>$*2q!NA)-lVz3KlCXB`p z=Bbf??P~6WlSKxoNNG%qHv67Njd*vic8RJ%LOdh=m~;E$nFzd3FDdnH_ZS#-%e22{ ze06bgQJ|2AT2S(5ME^)nTQqs0MRBqmMk0M4A2?(FOE*VMMFFw;UGE9~mg3_zoKPvy z=xxgy@~pE;DgE*%Un1bBnB`2z?M-i%9Q}PetVbQ>iF`8D5U$Lc*AP*kRsa zwJ4XKkT2evP+`v!(Oma_9$-rbR>4LLH1$q=C5cIhEriiD+S`;qMsM90vC;>_xspPS zXjo5t1JbB&&wHe^@0Q-*#urg^Nc3 zIP}r$J&|asOrAvl)i%@0xi=Qm-49B>pnGV=2N`|z?+`PSkB}xB2TV9!yuF9ncnQC6 zyM1T6$iR{-)3ImRV@Tyqaqtp_4;LB2z00YG(Y)#eC_vq`{3o&I z5;_lUbW%%IxapVduwTiES!SqxZr>=`O02cTM+yh4=a`%trWn!4&gSrmOvV>H+H8cV zQTgdJ5yUQMeQx}OuFZe?%wNY84HnI^9&f`Zdl$`GWHrBKj<{LQuy!eR(rj6?^aY@5 zc&o3{|2F@qpk|kRi4$0f*m=P7;p~~#&h7rnkie+sEL59`&`d{wUVp>|OW0P7rFJXl zm%lVA#=D4xD(&R;!bhK@A|wCAF6T5*e#q5Ksehb3_l6q5)BkjO{~hGZ2O7A%=kuoT zaeEBx+Rf6^&Ok}X_it5VQ}24XON=O{1k?}?MhX~7JEPK(UI1@VXVHb|bAOE<2W=r& zAD`eNilWt!S)Q)rfZBBKjD7QRK-3&<%=%%6C?Wz4qNoP45Atf0g87zJf1Take;eOT z7^|U#7@NG?dKd#bvrz2Oj{z;$vT54c%`J4f71YzKz&>8g;)|{wfQLT)U+4`cfxAvD zMWj86R&HL5^Xu zq5ZYwSNaO{cmLzTy8nODs>)IfU<}-U+x=SDniXF=tJAdn11h&wRnH`Z7j||MH_s#Y zn*+L#I^YPgc;emf{kLPZe2#~spWZ{`Z4I`T;gTPZYKFnl?eUp{FFv4a*~-&+b5T7| zK#wpCL|_M9cQCSjHwS^Sr=_K(-WNxd@(FH%K5c-}N=x)kR%}nE z$KBHBNtGCy&fCm&Hj(&7Do2JhwO30~^W$f<>j5TXnbJ3s;Z?W~-A#_(~)L2Hz1X@x)qR zvxVq*n2U*#;fz~;#M!E}xZtxEHID4<(VEgG#+)Ru8q%$>uhvZrB#|Z;j~ovBUT;@o z^A01qE>_kEF4F4$t868gp&GxOA`#~1nJpy>@4bD*3(cQdnFLknSm|ytMU5nOdFn`gtRh75R_VJRr)+X2{2w) z$Lu^%J~QAbSrtTK_K_1?ikF<<;{p9OL2-C+2O0|(d+=e@`qZU`( ztb=^ccO-Z7{&5#?%K`vtt}b0e%n7yhjY#se7 zppq-E^l5d0_rGh}r0LY{bo=IuwRdxkft+T&jo(X%>V&;OOve2=B`G|i)qRXEL)x$Z zW*4(8(7Vs7CNJd#u%fv*?|EPCrR`eDqV>U)Pvn0y<1;WDeA~Zxu<_v-?q+~ui^Uc3 zc9;~M54cK0BmHuqVEQP%)R(039LaXPN`Dh5m*zO_fC@A1br4wG*@r8SFB{n_MK7v{ERRzTX?i?t=~=K}t_6R4 zZ_VqMvNt)Qe~$p_!#C2 zb8idg3Dt(yhq8?GZ&h}a02-iL-;$Vx&l|)ZVA%0d41|o5yM0c%TCXrkclw!5H@2!+ zTKj0SNXt5nnyJl*xGeec&fG%f^|SA`{Z=&=$hmSoF5AMWi4M%73(Vjm+wH_*5_y%M zoik2ooZ`ik6T)4Po6s!FFKwl1B-YMYdl&UIl!qC{pek!l_ZJ)(Sc0)c)&HaDECZVC z;wY}t(%q$`j+B%VkuFIYrGTTm8h;a8mW;|0|ru3PNYSoyWzd>2fmMId+zFf`+9od?IVmpq1IdYfgtcq*1!gmmbIy^IOIK- zbznSntnjMsB;3$p{#+^KqD4UK^4ikj^bYa4snt*AQ5c3HL!_z0fjR_{l`LoOeL$m9ZYFRn7oJhaA86V{B}c)pV{^yimvP=|@yxE2buW1b9re zs?q!V1OTuhqNz!S3!3fmxXWTy&ET=X!z(;*3>Fx}?~T*L9bT^D1oL#r7@vJyOLsz) z$~P_G;H>ONm5$2hmp=xlO2z6TOI$=<{j@uqRHJ4SX6fiGy>M6u7f|Ivj-vrkJ$v+6 zwioa`3KV~^_mD^S7;uD0&#&@Ii1`S|DolP06}q$JcZ zky`P~ADM0u%{mCy+j0@QBS|SaPECsO0w!U24Dn2wM{dY9R+iUuvoz@d@9*Qq%7#qO zrlf=2kUuP(sRUbUs%!A_8_MlKZwU?K^ z(uJ69ODNan_T5U+qWwfB32Xtxb>6><|W}T*2FR{(2&foxZ&#q1Jube}QsRkl6 z$*9&vUbVg0%pZF5GIgrt*)q<90$`Ue=O2iUEqo-Ds#t{ik%>UqmoinYwC!GGBC*ZX zhqEYP?s^8xIM;&`(_ZtZQtApH$u>GLA|aOBWj}MagSdG+ZyzeZdMXcssW^-DkM(pD zVe>QHw>oM5A$E0+>+NJH5x&%9el^~3s{p!)s{q*MNLWZSrReRq9i6sd-CxFSMHJ0& zD)aNROa%En<;q7m;*laN_oC~!wl$(?)T z2JR)+Y4Lms8*%2}^E7eWUu2SV_1pi4Q~kPg>F6pJsEhLD*kYgmHtSe%RY@1HF@h^5 za}-C0q0x_}XHag;HwUA)_eQ-4LZgoTdjgU93yiJ-0qAj8BI76Y+7e7BffpOxfhXtO z&pY;+{vkjUomu`!b+WjDnCf|jUD5p@IkK;>lgWIfnBwAr8?l)ZIGUqAByRhwLv58p zt&)rX9HYf6rcEH~`%B!~1P}9CH;5amYMg4Z`rM)r>}ZOMc@m4oCjbEMI=duE67TfH zWZ3WYf*6{sF8v8s3$5NDy@-v#`rmN4kt#hZDJApg>dKU=5F9sQM^9yf&(U(r`#s!Y z@=?1NOw4frmvD#pb;z}-X3^Q)x?ItB^$Cu(8Vos#U6J?q^!|7{Xko0?Rgt_y$#NG( za@~j0)`z@}PI^==UH?u^K4Lw7oGO-{oxSpuJ>;~hlh&Y`pFTeDYV2*uELe!UN>f%c z+*{}&?d{f~!fCXd-1PQ4TV#3%TI<$U;KTsRW(kLSQ`#5+F*Fr<0uSFj*NEo|v-olU zxAaiq+a@_OU)lGY=(X2qFFFFuDlBg5Z`W>@V)7{|UkG^>+5R65|BhoU%^*~l z0o^#u+Bm%Kvqb`u)QZY@4^okkJu?$wimV7ELc=Cz6s) zrO&d9=mpWPGoHJg;*`PxO#zTm=49K9JeW{PzfCdt{y9c%BDs2pS$mhXMQA0-@96UxXI zq)(ua>+jW3;ociO#6t3@B=h}vHc5uVuF1Kw$A(|5$CuT(IFB_UsyTV^n*4F<#QjM! zxuWNTB%^|YHG(F!nr0vC<~(3OG#?M>ZZCXErRoWo?Yp+sX|mF$Pl^c|(s$ptm*b^F z90!;3^z8+|eH?Ax)V}zhZp{qcFOvTm5VxMOS4(~!D<7&Wu3%))%#!=pguF|rzFinmAhwmDQoc0;2e!W6!$!C3F-CVxXC^f%#5WRYok6JiAPNvg@)n6GSn=ke<{y#?846YTi?cE!yd)q0O$W7lg(ehc-fARA5zEvm?+U1U+~lO$u|$g zV#_IXfwb+7^p*|{uR&6QXHRegV)%(CG--#F>BJ9BAw=~>Vpr(NawZggAP3TuodJTqX_(m|AbLri6H7ibg?fZ-or9|t74om z5{lcPFy+W3^@wiIU!ZP3bDHSouuS#0h*EljlQa~_( z6TSE;F4jfpU7cTjr)!O;M{`e*ZYtdO_hqsW{6^nP>pYIzP9JLXhE32wMV}7r z&uR>sO^;V#PE?`V%8 zfknDkgs&s$hFH-Y$fn_+|9V@3u%@rB1R32bxrH#O<4M4TNQoQ}XZv;9fG#;xV8bim z|Nh~J=q{L!IHl->yj$@%QVZYg~;BJ0ieeSiz)6{NMmOOubo`Ur4f>g_T<8j+ip4g3( ze$$Ts(~cfH;su|~>B-)lfyF0Rc5dF1KZ&N&I6<1~c+7b{(dbbN=_c1h4AL2V*K^-f|WjiHj=c>a20ns z*GF5N_Dv9Kr4opGULGDm8o($*xC8U-`&$Q`;~?&@Y6Fxj}8# zd+R6LZo;H)<*-;B%FA(X%3iUQl2TD9g3~3^?VWCBMD&eomwWxP%k9kqxsO6_&=F`j z`u(M>+Ra>5yx$r`h*9fscZ={vms5n4ZUV89YVxw5EXZAxySgDg%N;!*B*#!zB>$Q*u@GO%Ho zFB-{KzwTJu`9p%L^}9N|3wXHfRYji}bV8-7$h?5C0DKzG`ngnv4 z_qjp1P|91Uc&UcHI>W8vX@RHPr&~<{@d?^BP6h1e9qQFHeP1<@?IiD6^No$Vms}S& zQn;mqucCrqcK``(tXdCKvMuD~*Eb4IP*5dz-%uEV6+h z?YpoFXNLkMq*+w4$Pu}hN?J+I!eF)>EyP#uM zMOllkj#~MFAsPpYSBE827<6$tN~rJn{j zQFa>ZsMJ;r8PY$V*97yBMpYld)o&@vyLWNp1;m#QZn{K?AefT!N6sdb*}je99=!SCrOBjxmK)<_@^<6>+*bVg5q!%2 zn|H$l$t@v~?HwJZ&xTUDUrqE)xTEbdaZ$oJ1M|y2HF~;U))<3VG^P+zpz{}ZsIFH~ z({!N-v%9~so(H=350iN7OnDknt>n3z%MC{bLCLNj0WreF|8L6bI~u3IK`8+i8bhSyQl*86X9u*TF5z57Q((@U+$6Vw%7;46=jo; zy7xJsQ1O~>$4`o2lAIXpMs7)U)=N95x4$0+&-lF-S#ZD!KGm!%D;wP1 zbBMhq07y-fTu^7oU2fSp zo-z)VTetkq7mSiJ%cMB~D^b+=YC37Iv{lo5iuLCT-ST9BruxA4rc-Kc&x01NAE{{Kh@6Le8Q$HyxU{d>aM3F`Nfc6Yv#8TcEQN<*JmJvZ)eFbAG$Hstf$1q8-K>-mg z^$WYTCIcZRoTq@UsT$v?uevYWEZm zf?l-^)PSqupXSSP#0Dbg>CnO(y=KeJ>U5rf0m}C$&~tko-Yv_{v#uf-?klx z9NQ7j)HntpO3UOvLb#S_jz9ttSCHD~tz((M^W}6dz)jp5Foe#{zno0cJ%g$?fyY_? zdF`ZVcanmLsB+vuHEGRLI!{_Nec_j2YYRf6xVbTOO7>;E!vv%E#{qzRB%^91MN-Q9 zEtyZBe~zaoDz}ZT{9R4Yr%e8Y6L&WPmYl{3{oT*Y%Nq-LkYHZ6{}YOMW=|A~LNgj3nWM?so>M6i`O6dvzV-G|&V}({!RzVkH&Ov$#e(;wQrEFQQ^9P-n5GJB zUV@D!LqoEu(j!#4<8_ezB2?xMzdA$4n3Xkl56|z_mkfdb%ZY%aZE|~pk@zoZq;&8} zYT3ggxk?d(hqMqPMJu{huR%v4WhhtSY}IGRur^BuB0QO838hVj@G=*4xphM}c{52B ztlpIPtL6fr@J&9E?^DayIv&V&I2pKX^Qku5_8%UJZspSuz4n-Q6~ zUaX7}xFvR3Z6}duY@6_|AGR5fu8vM|kZk49`QV!n-9gF;{hm+AEl@NqTnCtr z!hA*YCrMSJMcrol5=@byBnEoR5#KJ2#Am<;?c#-whQN-ix4+Wg$W`bN7%to{%UvX# zoy{o&uRPMC7mt)xNH#aW0krB7b&#|3QQ7r0;O#H^79~A0%d9Jg;R|LQYs2jN?H}NT zoceUaYE6OezWpXjs}wbL%olT>yuH%}j`Ub1WpImSZ*H1IwvnV77UK_1SSI z*v*869L$7__=-G03;&7XgmJEqj*Jism|s29p*qV|HZ!Pu_1%5ks+Sxy60vaLC?ckQW`jiDn$CEBMUUarKY! zcSp-zoYK;VEONnJOEv2!Cz%SFFR5{$aQurOb*jm!NQ&m0Np9_yX5VKSX%^qkNUn^hPo{W?Wyv6f&lf|%)ERwz%(pU5hOYUn-i&Os7JIFm3c`Vw{aK*7^%!Q? z_NZ6Y3zj#3LQY2&-fNj_oN9by=2sJ9X0u+7)6lqo`G5ojY@e@y3EEEeL#c^m(M7!pz+wLayI%s zuTz}CG>r%^cqUG*XhwD>d;RSW%4@EaKoP3?&C^Un{RL>`@blZ&mRLwG4gW!GxfCCt zj0q1-ISs?*H!~DgX5Hm&Gb8GFI0;{(NnWNGi}LVnW(pF~v$8HJ-u-DyYN;0(_%m^| zVAJo1!HA<++VywsK;KHOXYLmoq2sn1Keh*!9N$?O-AgF5AN;H#4ETwDhKmv;pj4J1 zaWlkM)Zh-lImHKm0k)_58D!d~~6>Q?L!PG6JmNR8*U1ub~pe5Gs`NIy79ko%J7D|9PQ zi|H1}Mv{CT{j56y{TaV!%ZcBWzd&nPx{Z{mO886^U5*RW)mu3u4z?A}^xS)x(0RM< z0??a$pQdBuFQhO3vggQmx&C60l4}O%Ycwv~i+^Tgbluv8@D= zRE6WjGveY~r@WWf?}6TbKT@Fc4`#Q+3S4*d%m6a5G?L{5)I$}9PHo1Ou*_6iV5`(w zrH8}z2D$T8pEbhje^Rv)loeO8>y2{e7W!=L9@b7FU1o zSl#II)EwVj+G15LIJLf?VlI_NyjDTdEy_#gX%4?m%mCr@Jm*q%mock`xf;_dK#8?# z0g_tUQ@1xE^-MO3O*s?GvsI2C>#-f(z-5l#6oYBu6C#_pj zs{^QTAZdge>Te+R=ko#9joA9Mre>4^>bI@=e3ZXK?Bxq|86#CoZJ~%alHT zCr4H%k*U%GV0#Num*fZlwHd%mKxM1VJH9am0ew24P1Is`B#L=OpCff5K5n-(2eTde zyn2h#l>z-Gx67&kF6Q=zry(U|P24Og&l8h?)b%v<*YNyn*V zhZb?wAZVL+0V=#JMlm#}CiK3^4+_e*LYY7(O4K2JEQi0nk4gqG)ElRKqDH`0kSw9! z5?59TK)`%#f)bCOP>z-xByHR^tSY?R>QVUTx39U&wFgx^A^XvORfNXsj{1+&$yrKG zdGs-wo~_JW+>{z7Wsl=NAp6qiC*SEy{WGZ+=ZM7e&WgU9NW0A1c4`=pa#x3xe^5hz z|5~o5(bMh01Vn(Noqk$o@UZs+c^F~Tq4f7rZCA6H)(uh3FK4ODUAPM#gV9&lohA-( z7Wd7oC$|effudMX9|vFzlz>3|-DLd}78$?ixl|mH*rWSzM`!wBFhzbHmt0Lc6p^w* z0X=p2-fK;)g;SHToQLcyn{mI=F^;;GnCg!ff?LN?_inH87xZznjz2hdqD1-x%Bg;o z%to7*#vvwfzglT;Ks3jMAJ{XzH_eK`Zz9qQ6SjOuEh@@a(e7+qjd=K=0%^DJ^{?a2 zM|>6D;uz80xkkC@@H5qK#e5Z~F1J5Ud71e{Dh+VoF6WbvXWBv2;havj69=~sJ_~wh zEf#)N+Pseub7T6l&0})RuhyRyAG#8r9>2U*s9d_f7#@97g9=XDyE^yb>3JiubI9F! zkzKa=K_}l}#JZmRQ^Drs_OXx%xFzeLF01!2+bgq$&t)0n=}#iJ#7%PLLcVX05I3$( zY@8TXO$S~ddyrgWtrQ8Z+pITNp=|@Pd4|gWoEY{yzzm+y#q)k)==r)g|E|w+#IbSl zk8{7L7^kYRhTb&2UuF`E?mdA(0i-aIB78m7kC)ac%QrLC}37 zGF4U1emBSBcU~louFGA9A%3oe$7{U*r&d@{p1kI+X)qNV@sMAj+K8VP?D)Tb|L%8m z#tsw#(t&3n@tz<@D{|jAb`uHY-9vSMwzI_Dp-!K2Y(G}F?*7VB(lfZ{q>~LjX)R8Z-F6YQD8vhr+W^@FiSoMNCq zrivc?8Y@rW2Cp zU_VdR-(h4_p4nD0=yA5~(%E~&b-NYw00=B}p4hXS>P{{;{|j zC8|I~5b88&|GxiSxRvhAy(vSr#DId`N&V%K)I3qP$XHm^K(UI)^cPN_8FACjAZ0WC zzvap8nNC)W_sCvWc9oT>3sM*DlUr0P7{VdbjHz}-Nl8q6swE4oWM|1(b{G=72cLfx zExojrjd=fF-t3bIOcDA~uG0D1-w$pj`P>hbPUW8$5{kE{OKmU@7FdZcfl04@_r=c< z%J_##FNIG+MMePOYPDrng-AikgzZwKzAu|NQbFj#kdw~oXBe!&LDNi}$y*9M#gDLz z3PW{ARR8JQdwY9>51BBlQ=4hv47pFw&Zt(qz^Zac1>-RtrH0vkF+C1UaBfm)nps(0 zO~*@M5oS7;d@9-Ryg{$__;*JTrp}G{z2@Uk$f&DnWRO2w)B_edzgXBD6^!3AQz`b? zfYbb2Pw=RHi{my)r41Q5?FIrd^KrvVs+kXWtbwomflWj9vQhu#{Hn~|fkwxFNWZ+1 zpI;?2PNlvhk_vh7k&O;|6D=7C-XfP^{peSfWhiGtH?yXEtiFJ;%j<=QDfj&3>$t;^CV1gxpl$_;~bgK?0)3xV8svY{P9MT z4iRV9dW35QLG=kT`YX^*@_w{TyU8E5_E@|J`Gr;-Be!uvY_MAEZUdO<4}z%N-wOC*rIqw?t}f0KkV~6^QOqT zjvzNTd8WQ2ydYWMtFU^I+wn@!^Z!^vQdT+3PB}6q@_F-wG7L=UwX4M2X6N3iH9inN zct~AkMHaylazoo`-O^UzjBR5lMUpZY2AB;*J*#~&v{Wt6`e_LXI&S_JB>>#>mN!_C zu%q3r>v>g^h4TdNWJs*q^#=mD54o<^VgMhRBEQ9D&eqx=A9ZO=DJ!Xwz%&rwgvk#V zMJAjdR|og4C@1(`?(iwXYfUR(s*&Te!$2qmdFahM$Y{p*QhlQXe!LbgUnjMWY`O zanh$5fC0%U!POl!jgl8#c-^iOFRWv?2y@&UB$Z6ueZACW-CkQ0&tVBtx1G(Z%t>Q`5o7UeH*!*0$xhqjoe{O2*R{vBdwzaipF;@LHuUlZ= zGb6tXjR?D?@na`{QKe8M3U^4NruH*fQU>|rp@DJF^nGBl{cJs0L`|tBM))Nsv8HbgV)$Fb69_8~&-+a!6F6|Jn|t z@&bqZdhmc-A4HGsGaVG>JHHrr$Pg0<_GM@GmwNC*UFgN&%#a%sSIR>O3gOK0O`*wL zsi#ILk!S#uC1q-oEJ2mOX)GP%6}mfs@}J$FZ77EKIHQ8={W~LxKipY3HmZJwU4<7n zD;(SS<&~`}gPj1U-1fa)!W3i^{Ofs|@dn%*rjjX|{p>~Vd6;Y-<5(2BVa%W#3zn+Ebc^4g?WvyNyKTw%< z+wfF4xnzDFbWUsl9VfPYF7T#l&dnzX9jTpXkwsI$N^wTvZBE4rAhcN+DKJ7<87KGg zc>=(zYK*-)X+1a3N*{Jd?{?g>ppO5N;;t&H=F;YCFrlo)vT98Q=61Z3Q!>^){mmxy zKd>DX_BQer@;DdFcOx1hDMf?nHrjdaBJ}FblCEH_v7f{tIF`s%;~f~WJK>7!QTcAatMc@OJN7pCd|u84W_IAYtIyIKKLVrQ>Z zlw?{?9LjSaqu!_qUu*N-|8RY>$0EcKQDCd2k#&92Ws$ds>AVF(zKA~0Z%w0}Hc|`j z=NEkKzu%GO1@*cLV$Ua*5G({<5?{`fREoCC1KWOzoP{()!FZeh`G?KT$L4MRgE6!M zW$4WE3ZdG8!w=0ZkZ4Zcn8uI46>rAPb||waSep)8A_8C)lE=n=r22i| zavs{8sZO8&t<&&m7~zc1M2q=qSB_hmXjXtrG!UK47Ox5x5FQ!Xxr^cCK8Ku<^pH7aIb)H%)ho^-a~QHVs}I4n!|eo28G zogh@{Znw_?P;{4|&nai{Sq*4zNXUOp%NTm$KJ938fdChTSLgK;1+5jn))H2BA8+Z3B z*|I5-zNXKDu6(RtjeM$pw*$wK_a(F9gNmP!k%7r<$Ien7;T>leX!WY0(F_jUzAV}_ zm)6XJhhwXmGen{h>+}KrXN_H{a|HG5uaF!s^quMLow2&G`SSM&``-%&kDDd{#t5clsr+|Swp_dJeE(UC*hC7$>YTh=WsvV7BLLn4>s03% zGCoOY4NDoM*Ov3;Vl5Ts$a+bCwY)C3zPgnv5j(ei>ewXp)fsq?=_0~tn*jhs#n(rN zDw)gbBW!sjYsRxRIaDHR6bP!)HR8{KMxl4bRw^v%&b@=WH}x{E5*)TvO-(+-wCewq zY8Md0z+cf3%w6q-S$x%b!`bc}W=KuE*vP9gGc$9wy>P>Km_Zc*{EbW`>DxcvG#Kkq z)g3nRMfN=}ywWE;$peJZX~qLF%YWuJI-g3$D#~$3_l>?Ans&5Xi@rYCSu-(=p)N&n z7xL9YXzC@0US928gLJ)Li+FcgWdnwMrMr)r$nj_-G4pVw!3KlT)zs^2>b$@s-mDQC zRPE;>pfjQ;iU30iA*VEHC!>nSwjhu2ZJzGgHIPa(;~BuLL3tqkt&!OOnm^u(68mmZ zl2O?AQ6M~!zjJa4a%E(p!E9*VuMr!bL)P^m*a5po5I`Y7BJm()Ax|=|wt+=vG)S~J zzGuXQ+Byw5xx@lv)Vp5{z!-zQrjRhV1CkX&Bp) z3g&j`MwJvSJ9ttFfogK1O~woTZIX&tkHPz9RE@oC{s{+~Iq~C@5zo*)J$le;g2nRO zAG1fCuK1(gwVw#QMos~i*j*MSOaxJAOMxaWH}fa3$|Z{&PbII}6R0W0M2rh7I^oS6 z5-ihAHo}PPq-LwYk@{h?#fd#>nZ4*Fpd{L|-PmJx+qN7@?KNct*TBVo;p7@9T!7hb z2sibn15E=-A^SvGv%4cr=Py@aV7R7gCsN5 zKa+K%K{lA0j`^lic^quc2#)_zW3spA?UsFG^K-A=oHo{k#(6|HRrtSa{rq{pdw&h1 zJYL_)nnv5tajB_(t5Q2R{k8j6zm=s~AW)7Ee%c9NuQrIoa1GgZ?nDg?ysVtX4|g&# zLUfDu3fRx%X-HAzv-ArElaYJlibRC2^&KUAr_Yn+sHU!8!>wFg4>@|0T5^`vdEV3( z5D?nEwz<9ll4?RN5^Pvy3!LvGe42j!`kh!pUv%?MrxO?)W0jRnvtPe4v1TMF%NVuZ zJ;bS(22E`Xvcq}%m6*lsQr^wV?YmLATby}wT7krIXY;{64}d%1;9a=glZkUyNyeTe z_ujOXww9(}dMcK?D=eA&CLK3xgkk<50m$&PM!?VAb!dE?ylU6oBLJjJ%NKooTFX=x ztl|R5kDlW^px?h#_&ztN@NZ%=GlJrs`wIF?TJ@EG0$IMKPnym&AGnMLxza+ zSv;c5Z~t0-8qMcM_GBwzuF7ZFxm4PJFR^}-(s_4>gX*D-pzKro*SW^CV{RJ1;s7rm z=oG+vq{Gdjsrz)EpH=ocs`KJYr{x%cyI&c`b4q%ZbK~&|4lIt4^G~$ z#$49EoM#x-8T+e7(xrrKldndP+YbAR6whqGnM;l*KcleGoB9-6pLh)x?Wo3mQ_@;? zVM0@4dJ!QRPIs<4g(49(uWGxs#X{L5+1xDRULJm}ti zvqG&%!FZ61ogNX!aapBJ7RE97{yI3a1QOq`z?EW^6uT}Nfm{gBkU(%+79!cTp8yiD z9sJ(QHNdWXR`mHx^n=k_r%3ZsEaQ5NBTt>F{ui%MC%Saw0(54RfH{D2zTX7Oysx=E zY)7EpW@i|2Z|6{_yy%goU0>IT*B*!_N}Yu=q3jxJEMJW=bPIOxo5oJ}ziA8*qh}N= zGfv{>-CTu^VygyK%luwRAEz8PYz&|B4m>c0!mq959X&L|5f54| zD`9#53xldcpVn)qyOTtT?kdOJTUSoau^3n%wSU;RBQL?^Ue})@tz%?4O+2C2mZgCWr{;#n|DpEQKzNt)@a_90!&FQ z?NQKlsH$s#7(2?~+skUzIvG&51>T&a?y(ArrmT=_=gAg~TYmcqf)so(r)G%hLi_fX zmV#-oFBChLJfVd=K#QFU;~fnJVQ2l0Vw9OFx&ZW0$U}jKe)+(ffJ#U3hVdxtqckTM z{aB0vFS}QScnH}mLVf8uYF}SmL8!EHmp0+MkW*I5q*d+v42H=FU7P0I8lZ?+TZ>6c zFU`9uef{S7+}HbB=;z|1e#szT5tI}l0!_NDIiFH_Lf*Co1Y7?AwN?B|${IZk=+nd+ z?TWrIU_4(MTk&9_>IUj!=e>LEYv1?>psgio?~-LAA)mjfIS<1*l>&()Mf`T9T1P7@ zGAYn*_{)i?(OhAsgt9CoA3Q(|W0s}E`??reRjb)&J4>k|)os96m)&)0`;|+R0)pX) zx|a$ANWPrl$mRtQavC=7JG^>ECwE^cRTT=K$OylSnU9!q2fA$F-x%BWRGE08R0@wO zB7(79=xJuGZIK4lL6T%5U>5lcy8@ae`F0#pQusW^6?(Za{T9zc7|6jP-Aa3G-Wm9D z+%^zX0C7)sc7=TVVJ7X%R`y6aScdLCQER!wK?T`&XAt9J=bK7(e;%05J}$rH7ib8~ zDaOK@vNgpN@q)W3{M{IFfwZWBD!jDYY52(rMkap6-6udfiMBX06hO4E19Tsd1d@3A4xnc_LGNyu19@_&+N@6o+$QMRTl1K z;_FQ%bP3|v709A)^WQZ3-(B?xs{Sozd{q4YId7bW3b(&@cAcgI0;@5OF~TT5W-W{d z&Ap@jH}<2XIE}M zkXGACM^wA9p5Z-Nygo}Yn+~HoW2dt}n=XFG%c5R$FVjSUp#h1l&=PpP*2EE+s>O7; zRFA^$2)D4EZWYa4cLN{x;2>FK#Fjx2lEt$#ws{1F&<@iQ9z5u2WFi@SFJC=68gkvCuKHC6z5BJV54^*ZR9`p@6Fkb}M;aUF4DrPiPWnS^oI0U&8jSxR>l0UO_O9~O#^7cJD`m8cWY~&&i=oZsi#GA(qC2#eo^r=ONtE_|=NcRI7VFT<4 zU>sScWW+sc4dN;Vw>mpWsqWT#L8bU&o*!Xl)ye`;7>1&lcHcqiQDyPWd24mEPhfW9 zDiet;x3#U&Ac(L00xXnv_DOB(Zz{>`w`kDT3-2l5G%zDHq`GdV#<9x7YZ84bikG*=kzmc781OiN6hYJ`L_P&+L4{f3#*J6bS>o-Mp57!p zRPX6Jlt~t0-wQl={yU^q=BgptdF)U|ez&1uBFVw(QOnkBRouD+kAmgm3tz52qYrg3 zJeO82Mi}SikIRJf4!s|8?EAkKEO%R#_+Ci=o9>zWdM5Qwnl}G1C~`)GVv19bP-i34 zsRDBuGI=#F2LzOBry*A%!51TK0=gx7W!H$Og08J}rveqJYuh_amZX8z;w5|5cKq$l>uYt8KQ(VYu)<9$Atm+aTMuC! zWC?(s^-H$MODO3lcz9y6+t!XAm4M(CQ15;pWqn3`?VLB|IMN<~(x(BqqWNHTICrcv zmWIjrw$ItW^R%lIpO8^s;pNfbLr}dRBnlqk{0~n5nkGJ~&Td-pQckKjZ z$yMd|<41U*vcRML?aec&A@=)GTMXnDEz-I)^kl4HZ2s|QmJ_Le<=;+C zmnFDHp2$y#WnN#650#OTKI!t|mfEB!XWNkVhT2HvtGAABlK0UIOdk zK8n@CgbO$!Fs)LENqXpbA_x6w=Q+G@xoh98_w~(MUXnFIW2K3-ObQ9J2X z)LFc|2?+e%J~8daq=Dg3C4FPBv?Aoj-Sw3!-esKC%S}24n8cy9w)RBIMZ*k}$+WFg zmRB7Y2M)vE@oujOdm77Lz?B#Aj-HyuD^Y&e9_1XrYirN@Gy?X9A-b-LXXrlqYofB! z%F}pHCId}_0pD*v%o+-{%~^HnqZz=MXZ(F|;zivfFRX?xjHcd?IvD5Klb<5~_0%aM z#%)&f9nS*rl&y#`idpxa-qk^(>(~ScrytkauR^upFemYX4bKOrquq6DU3O}cV~&{Q z7w}a;fE6+L?7*O)w-Hg8oEn2na;AqR(n=ceDK>p}Ac>S|gdP0-hDC&92k%%|nx}m0 zsP0>#_E2#3nkCNj#b3mH%cD&1RO!FOu4Z_cueF++K^C8+iLn;yW?jT8tO-30CR3fB z-gE!=N3nl=6+ez?7RtZ!`U|rvS7eyTr#9VM^C+1=<^vy`Uo^v@=|f#&eYC2Ri4EP^ zeS!VQt?G@N#D2*irt40fJ>;A)2>D4?qCJ_7KUsB2KRl^m20CyYXi`09*Op-ejF*b- z?K734B&*h!aV))rG$ye`;i8XOOP08Qb_$LKJsLcQoWpTtZNIBkcDNM)%sT+~ zJYb=MFdD54}9#p{l-u(ext`*sca!x)}gvh7_ldhvl_pfSx7bBOZx6laT)$G|_Z;5!|Hz@dz zqVw>l@_pm@SE-a$_9lBAGh_=PGlcA{gTopxu`Fu%l1mZR$ zQW!)78+(A0cfiU+FRz!r`TBn799Aw#TBa^;Hbs%x1E`UbNH~ z2ASTP2b4*9;bxrRLI)NY>iJBgPT5%a&aCO_=_^-KYU6QvzruzY(;+JI<-0REUJB#d zwar}3n#u$h1c-Kb?vNF%A0|P%EZc*qss&2R(14H4J~7jNhr=!ZvEv>P|M*VxBAI68 zD$Nz)#{Ik@`X_C|FM}c%uIS0ZD?J08o@yP;!vd=P-A=~ws7^=trJ|yQ=V6dF{U_OvBKP{+4-K*i1>ymW zebAjN953WtX1gE*qNS>G`YOb>92(915)@Z|;aQDSYUZotV^{`*#j-H5<4({?u;Z3D zL1Ab$@><*Z@y4{*|FY$?*^?Xe@wD68b4KsO+_C|CXTp0cB|ZkM?7QvI^fgh30h;~s9_pQ-9# zhk`$`cFSHw${N*kmwsk~c=sotIkx&FtTuJ!JUH#&R`Tq!YGBhTj(&Z_k*s1W@dKpiuV`Bzl;>7)53AGH+vecy%?`@}}in|MrvVTnTt$6>&D1u!ZpJi| zERz5D5&z=HMBeuG(z5PwFv2}acQPu0r%cC>D$5dMYPo2VapxpP<0Rj_<&dwUEy4H zQ`t)jWUsj>`XG1OCQj$IrX}dQAN)PB)7-EELDR#>G0<QVwJMj~nqpL#F zQr5SjNYr#ptI6Y-iLu9Rn~>BtI%ZWe?QZ(-5{c9%c22h38?e8&2J{znY7~1DIdbD- zg!OsW0Vl#0zj+1sXSbkcNK`UcR77Lje_Yxa5XVacnkCOBl?S7?xK{Dk4d!_sYBnCm;56RbKn#b_2^*SI(Tq(&K8 z?ig@DFx`CkTdo+|#)(TvZryJ^WV@gNf5N?qTjP*><8e8pXj}c-|BBC0MO-$PtJ*rG zKhzTsR6?k+pAoeAXi5Lu{`92_g6y^=kfw?STasTH zJiw}KZG*QgB1`S)<`Zm2`EQ)N#^ZgGFkY6!{3_9E;N%X6Wi|_8ycB;Uw~v?>&428=8Id=Pv@sx%~wl4 zRPT=Vdo2zNn0tv4FbzS`{=s;LQy{(WwG5#41QiY^pO+h#R?7L}IYqXop+Oxc?Ta_; z=7N+5`+M(30Vg=w` z`XA*FK2k9EHp?S~r(&MV^Gas?+9DLw))^Yy5l7f=;)!Y3n|%_|(0x!vcJ^8yaQpB& zgqP6y#Q0!oEDv1V%P#vo2diF)Emmg5U(H4`sIe;h zwa)w-4N=lGQ@bAA0;R~}GO~~Y_EpvJiwLH_2JY!dXhc>`6`5xYt{^D!gH_g+xh<9} zm+Gr=dcvgds;wH@YCu~0p9oS(2l-pR*S#T+9e2wHFA&8Jh+K2-XxqBiFR8})1mloug|Cot#hyyr z^k<=!*y&e4au0|<<=ytyo-}9tJ=J1#v3U&)tO?2^meuY5tKlk!)L9EAc6ZF*Kh+vX-XY&|}jK>BHAB&g}inAj4) zRk9}pNq%UXmcRCBYi=$YX7*yI!C{) z|9z=`u{$F8zvuOHJWgz@(-mxj(KtiP>WMpm4pisxi<^k#wqfBmrf^&3Z^80L%em-W zEfd2*mMMS3%IP{8<{6u|;K!S}F|%9)G2s!zNwwjVIGs0~M@Yxn<=J+f{rzJsFd|4f zl5W>}TAl@Ca6;fZEb2tTFK?u*^YdNB{URz~y)47**7}|yx8`Wm_MBV=JMoRO96Dc0 z`@YqTq@O!6LO>ng#Q*WOUV1?1zqt62PLs{tSFl~Mi#R2ZlHTu}Lz9O8A7R)E&V3Gp zeCv3KWgVG(Ld7hPBTb^Z^&6zlbwkUAWMLQCu*?6;VL`PUTH22_9<$XKI9@-M#+lwB z6cn5)DKm@&zr+WkkmvotO@jiy>cUhmtMTNxOoLH99pnz8n|atJ0%h4W-_ytXgo02} z4;lXF9uM%G6d+YKJ2Yfzj%eF^^2ehA?2qq>qi?KSJ^7dKD?MRiyfa60&xSpH9#=yi zq46%(FFca&rtdCWZe$&u1dx0)hQ%Z7IXxV6I9aN8KfA~k=Uy=}er-M4hlF-`_t$** z)IZsBb3d|y(#_=6c!q6sZU&DEBGVwdE?8H{j3$hv(ZS$4QE7VS;bHyi>c6Oi)uc9? zDC&m-_k{4TU){5_pM1c5RfZ~J?~jm+FPqyn*e#BlMf~xMQE9;t-wXdEP_M#5Sjq{N z*cPSx_@RYWl7<5R(Q!TP^t>B)anib97vABF77toWRM|M)GMIs-vSC1|88ERPc5?I{1fz|zA910Vb#GxYax1hTk zE~fbi?LKq;hPa>a>t2TK0ltGS(7bk32SNW%Ic(DeH5TyEh7Ogz%_XS{Fp$X=)Lr}n z6QGF(y-;lfypEW?XRa2I;WkIe^+^8sHXjYXylMuO#6nx8ypFfu1(Ne^uvRX0K>;>2 z142Ab@}22Fs+7De_9qv6_RkydE^h7p_QK%}y;pN3(^}qtb@d^-&)%gB=)xQt7qFt<6aL*d~x`+2gI zSHDO&!>GM>*gw5uE{En+G=??{p+@5l`y29}cVW?!z<<%j8GtfZlTbe|PBHX772g#J z=|;Ct$`R|m-*A^(!H9FOr$UmD&ZuXjP@a&gZ}s}$clF<}vul7o3R)3`dmMU`^oMp# zZ&9{fo|J~SKP|@Fb;uvc;~LX0(k|JKvwFPJ8;2SwpSf1%8#Ulh9rC)Jc-iQGft-d9 zVQTiuAk-SzCOsz;_#tMd=8i6I(Xc5X2~_BKhBV`E`e7~@F`jMVEfX5~4Oi-Vk?C)i zRg2{QJXO;Yua^iv z$Zg*Vsig|4fm;vorXaJ}`S898_MdkowpiDQ?}I4@fJc_8g)(vhM$AQ=K>x1;`$04xz4UReg^wH5U5Iq4Km1^-XIjiz9dp;ndU@efWQAHWsFh?-* z+8-Q&9{+F%{q~JGCN>7_MSKqr(4Cq=FksWe#p}N}p@9%Zu9+rFRcxhvy87skHfv$S zF}hvm^&dQNqQ?N7q-CvebhiLpcpE=NYqQ>}2@zIRU$W&^A0!jC?TwJ{8r%U*Q>sZ9 zr7?s6FMw?T;Lh02Ng3@kTjBcupCyI~a(48`x5FoFYA_%TKD8^HR6Vg_^O0 z8bBO41(U|*B>!g^@R>b|KS4?7k9nvaRZ0Q;1dFx6>HP5QgT<}&UFzK_F1nl};ya_t z2)8l%zlfrzmv1NBEx0q z1!onoAcS|_%i+}H@p`ERUyYIf$hlMgqXdt%=GjX%#me*Mvy zpiGrd+lB8CAa%Hkkq*!LFZlbd8X8a5#sor@e-3ILjo_bR<)JbA0P0nTCFRhwzPa`NM?zJYncCN4o$! zXPdrgNu)sMhd-NC{Q+IR3f@yodReu?+?s6E+^G|SmK$tWZS6|0TvwuOShOkD8hcHQ$%rC$j4mLLgP1 zlDBLl(e0}GH!o*AT=X9yT-fKZk#$%(UaoIv5V18ERc{L@mWv19*uH=NKC znCU6z8WYT%+DA%|;0Ta-0@23){?Y@Ge}L9t_AlqCH6nUJpNV9A z$Bn&^Xk2j9_5VNM!8G8D0J0}&kkpwt`a6M4SrSC0XDrC1M&WFxMXgnqQ082SX@k^$Xv-aP$2)Xy`ntE&zYQ$q_^2ui4D2E@u1y*} zdwr{O*m1$>M_!XJbx4Ftje;N)$UOT{B+vu@;ZaY>yI0b$>_G4Y^gEqhT$r#%cI*^? zcfBac0t92=uhLZ3)XDvCMv`#SjRNnXt2lqj_P4xWHX`sxxeUX&su)HkP?KTUzYoU{ zM%Y^-myoV9w7$?E-?b9kY+PDRl zC=VzAw$z&I&_t?7ywDw=01G-I!m0+r3gg>97BYo*y& z*z<^0M1Ib(MghXUohaDX5^Qs)u06?yy*c%%cN#Dq={WK$*Gbwr7a3Nn2#|}9yvdE$ z`s*$pg}x6a@f~J~CcIe~a&v{LXs4UcT9eGMh?Peo)qy*;6nVjB20Pu_#BPk29sa5L z?4$(_iX;Urw>EryITl(@pXn9g8J7?G08a_s?&zmLqFpfl^xrpHjqnK0T%E~(j>zbTj*uT=#BwyJ@ z3fhg#n28x{u1a6h8l%l6c1QQzlupGX7&qFsD3NC1=@k_fV^z^ADdkZj@aaPleb0sM z1KCwStA+v|;}Pz!~dXGj*T8IY5V zI|2!SQT_&F*+oV0@HAg{Ob%GPh|OW;ZIv0b(GUQ#ED=;+Rh=~`Q&H3M*xMM6$>ppH zbJjIHy$V`0@9=JVusB`ue^kH+?Xl77%34y7*@eM z$VGl}V(SegiTeEiq1MC)pd0H?qJdIX!)M_J~vmVTc;ni zJp$!4@z+j0U>!WNZJ|%5+~(_!Zm3CY@8aQ%KV~r2H!d^c(dW^n5V5g87<|uL*KG7G zgcZP)Mx$JA?JM~r9jQa zKB*^9qHuf(#;%fXEJ^Pc@cAn5853jEHE)NpWjc9+(_qrwa?AC{WHmZ3qMy;CK+Iak=hc|9)x)DO5-5xK5m z@;c>nJ0L&<>U}gs2fhjNA@+vH_0P>Vu}DM4rmDY8$lm9}uf7WMLq29L5c++Y#zq^& zEm8p->fLBX`=>=54TSIEk987SS{@AU<`q9S(R|PSpNx`!_Lly4>DpFjNJK#^p)b6D z732$wHdOxBciWD?i@35p%d(ls=4P6BS`fVvPN5p;>3}Jb~#m zP%Cq%#T)O$m#Z{e_Gs`u-eQ|-epHQLO;lE!*_j>j&(u2dK_5}j4=84=yeb*lWD@Jp zvLJgKoRI^1H3MLq(0`9OdRuI{u*mMOfkL6ON}JXb)qej^TLPG8wnxN0b=&oOH$l>K z7Pog-tQq%-@1GFU1P5`od7R0=3bu1y9h`&xjcwXL+4T2elI*FTpF*fPhau;s%^&#Pa)jLGI--T~8 zARl}Y?mdcVXb^3%Z#S>&7BEI-nYO%58YHVl$}I-}6ZX-*Uya&y^TxYk0w2mcJy2~` z*$n=kF7=M8PYukAVl4PSsFthpcQ4G( zgV*CIRduQ)(2aKR{oDpfkY3v=MkT^V|C^Kv=dS0|M~4$CaK(rvt17F8dA`k45oys} zm}EGhz{~YEt8b?WzIBoaRc{I1F|Y*xoZuuRM;sxBS&h(AK4Px{Fd6oIK3g>>0Y)|@ zp0_HiqHH^iL6nBQ0&l@Z(*O+50cZl;6ihK>W)x#vZu^Z-Cm(aDoS$xkbn37Z&0v1I zMAwNL%2!dx%7#jUcX87uVEfXuBzC)gU_(HRHv%!bR8F8^^T zqKU!2+J7-ZFOwL^44p`xhCVOZ2J%T|u}{T(_y>i$(dR`77#~e+*dzXwl*WNfS{Fne z$(!sd&dvJF9_J`s1Y-xb7lqxN=?s$Z1@*L0Vl=jyVM!$UrCv!O`K6_ovd;S;%`kdw zXC)NewLt}rSl&Fw8z$@l^oz*Y@)B%I2lhaP$@at9JsCLhSi_ z$}D<>i5V{s#i2B@%?2)rO`N6&*pe1Mn=_&)~c;`VA! z36(RLN3!!-D`LXCW37R@`S!1&L+KtMtJQ;@VdgKvw3V9aP$~U*=c#{NA^g^C))Kz^ zATAoJ4ILaj5#nwB)Mi!1r1Hf7F0@n+qB{Wcxy{~#JFYVV<@Rt4Tg2t6SI3U4g3bi^ z&$W4K?%qJ#zZNy<9q0gLTi*rMz(fdJhP}Dp;eEHT4X9$2Y=e7)EXe~S;|~XkcboL! zFhI}Ol!{u|5e1-cOJ_WTcZsBF+gWadpnJX*8#<8a;vUYY(O&@0?Z66Evrw+b1F|Yc zMn;R5z}svtN_KQXtD|Fzd3a-z$R01v^uSAQ@lrd1{nG7zZ)uzfmD|KvWwmb0W(*LVWwk08Vsw-3la^hVD=`= z=udk?c#UE6g@UugDV zd#iQB91;2%EJcel~?rZ==+IlpFhx(8TCngQ%UDw zQqLu+W_colai7_}V0+ARW^5KYR>;Y5eb!=~BD+_|o2J7uTFmOoVb;P+o!EAiE>A!n9+;ST; z$HNkFnZcq>3w>u7w#*}wuM(f1BmZUFOH~~ZsXu+*@ox0;kT;gm>8Oa6&izUC%B&`{ z6*Cyo_(nuLS!0#*_c50u)R{s1732?*f)=dNLL^zP*)Ra}Zmb z*nY_jQ)k-+O!bTF@nI+0^e*3_^YgF*TM!MlDmOAqLY01Wl}lme-W`JSQIuiA!ew4*W+=u*`Z>~wZ-ir?`{0B^bKV_ENOThAYxp6UakQ<>163b znJBZKdTZh2N4-6Fq7YR%6NH54dC()m^s;Fhps&qjcczo7fBMNLbuq_H&iExDd21rU zv;(9s-+fB)OB=hGJs^TrzWnCt-{mT=(_~XwT4G0~jr{{w8nI$Sx*ZY(J$_ta*JHE45)WX3d8a-jL}U*U!@XoC%K+AT1X!KpYl0dyfg}B`oUe`s&~$2bqA|i?RRXtg$d+%nKktT0T0RL>L5K9*UaIsDRR;;K-qUk-mau`g zjJL^&IbIR4h~+QrdH*g>o+p$m2PXe1vi3OF$=nkV0RR-xb~+kwXDr z>O)#ICM65jHt(iWp|nM6L!`-20Kn!Y(#ms!Kw?%xmzmE4sg+Y~AgBZqHH ziTH5%AKZy`z6Q$XgB%}gF`19tDRhy{HRnsEQXK#hnz`c0ML9FY@w;=1Jo^dYv zyzkz!h?NN4*d`T}T~G)m@LMlL=IaIpJK50#@Ze7&qc<+G{llOJubn~}2)1G$`d)9L z&wo7a_^RGg!1ZTaXnXAm+Rad-fnDTCf~`~;q8P-NK$aOTOp@!c8S_w8{q25s-0f)C z=s}RD;47|?Jd&9U=QdkznURd2{Z#0apMs25hmCi<#-9!?rjb9q%v=0UrEYfis^$k9 zKiM~u4@R0|r>Z_owiI|l$y`Ci^5*_81`_QQ+l((6`oCX~yuJ%&_=%kvq|AayCW_a$ zgnn(?{ID{2G~``jSF@-7Y3PqB{SgjM-2Ird*}2)-o0&SFovJ@)H&Q9~-v%_oF_r{n z+>aDFA@-lg>x+_N*l_TnCK)dGzke7`ELz7!i3Dd^db@Pkidplg3D8qR-78Hpv3m?7 zwEZ^jy||R8fI4fEc%~`plOTXTx|&@p5Zg7kJR!O$_*G3QsoNwa_EN(=&LAb3pSD_~ zJHvOUBJXFtA-S0ZN`_P)ii)E28jwU~; zR4SF*$LbbF?gZI9Z$okPMX(QQl*koMLYwvC-W^20l{VWOq7~cWo=^K;u2+;3f59m!5SLON)bN;rQ9OtrO?v0wu^iAG;v zC$_Avt_Ecl+0=-O^!>j{Qt|Bt>x!nIx;AJaE!FZ+Rj_gt5@lkSZ17LbFz!dUU+s?i zp^92v?vF8BOHf)o?#)(=1!ze^Sd|#)?O!%YRSO=tD293rhGF9ILYaWq>pS2r>XN_@ zzfU_4ip9t%2PpJ3JtF*jUMMuEOD>H=X`%)WMLC-7r+>%@>1VC$lK9f*pR(}dBuzXj z+3<4pwuht5h>P_~)=8i-Unvo)c`sG1EQCi>lr5gjS#?+nS_%J%v*y4AV(qw7!&LrH z>lwU4VYd0ErWxTucn>8dhGmo@M=f6?tq?jg`Qv$6?MKErm7A^xLW1&T({Me=`d-@J zj~!ZrL9&%pY8v!&Jk=zZd`z}91t1r7r>;z@<`ESv2+LX zK7{NMNrJI}<*i%Mb^3>=K4{Rpalfp1wZkg zot9?6Q{!_j99Pgw5%(aNlAg*P-(pB;t!5dt3S)VYwnr2qRb4z)q}%-e;+9*(i>QB^u7&v&-;OBl9N-Bv#+sr%uSPfFn-Q0>N5+k%Fl!V7S>ot7x!K4%#_NgkSv zVtZ%7GJ4Nme=0^Xe#53;wvIriqHT9q`0DD>L+lI)4F-~&zydP*uEb7G&azZ&R*)a$ zZ=l2*qBBH#)pGe5`KgE}W+49)X8Fy$LD=BE!td1vIR0e_$u|~uXzFB55d|R|1Zn5{ zlFz5Ep6gkXarC?XOlo}MXr>@hc!FRSU8W2og>e{YYRu~N%(%!g&QXL#*f1^#bSX&6 zgMPNl&Kul3M{w}EpmogQ7AesW^}kwh$O#xg65-1|QO(h*bQrg?`{@dWy{Gd$p=WzU z)rOvC|HUx&zHsJzXZhg&(c+NonN&Lm4;_7~{+PX`+K23OJEx8`<6pU+$0a1|YEY@~ zIyWz_dxPLjN#K7(V4GW1jd&t!vP{6Fu`?bEkfC&YMK8dH)rbsI+ zu_sgrv&bNKr#*5ryjDH4$CmE;c=1B*k!}5k;fH@aH3g?}VPf|+?~s>&+>HN;&GeJJ zXICE2d;?pWAYgHlBvLJ6axHL+8&5#4(qQMAVl^Y$U-=2Q56i8#L%wlhI zle<w@IkAp9EgBHF>496qvUrDOVPoQ{;}h zhjEfoF^Coh_U@zc7engm(On(<1*~-)gQ|29N51CuOYGv?4}SRI04!n1HgYbhrqA z{+whD3ZXzyeBv#lHELT~_vw!AY>!?OAzJM^H9h^jA6IxZq;QG!+jP*0ZHKE^hl?zN z^ziiL!A3qY1dEVvzU_##e}d!}Hdq*lsKbOXl?Jq|-#9=6sJPfZB|5I7gUO`DUyccf zgL5U-!|p#t&DE+mJ=k_Hsh%S{ReMp`EZqNZ;rYjk%rgXGZB=4u9~xSr$}1}5^XD?h z?aP~*PId#RL!SRAd7MPnKBXb7+39WZ$EsMj@YIbnM=*}ucomTwGPeG{<(%fCqvd+x zTF+oCCt~)pg){6AKd8WvW(mZWS4o||8ofvDQkC#Y6ZRf zv$!<#XT`sAOnp9u`Vj89+%+kJZ10!=6PJqx#RzLdbf0UoeA?IC^=go|;;)@7dbc*U zs^7*MK{-eUnS2J#9hHsy)A?kr)>Gz2h?A-|i!p@_L(3}U z^fBUDg1I?4L^F)HjZ&5boWj))&+|cLIIpFhbz<=kKZ7}*rnb+;O~%z{1pCx#oTd_~OP5&UK37xfj|}TTKA~KR z2=Z%QUd}5`rI2)Nr+TxJ%Bs)Awtf!Gbwf7lW+f#uCilR(qRkziV{vd&Xl;aFdLS8^ z#Xh~Turbv33>in$A{wG~)Qe8vfB)$s(RV|9LCB)pYMl9Mimc&E95CBsnIj*!#|4T% z($fNNW@=E=@;vz~6nQ~};YYAwbpQR^b@aVP@;0BxqktGLC(Ro+!%a*<%l3HV{f5!J zQR4p*yS{?e^W!9T6aJ~c*>LjbB=tn#Zy;kgIKN8(XukY0T%)>O z=0c@dM;kn-fMt%WE3s z3a2xQp6t^68{es#o|!Spz z(O=8vO5O4tbQI}CnfiF7-iRAcm2|nfcVJc~qq>O|Rw!4#K9gWk8o0fcDzW&M_fTWy z5msO^_o2kb{L*vqCn)KTbBrG(N%X`1Ko0|c@Ercy(ye?9mF>SNrLz^b$ z_2oM8F7oaY-7R5HunbYBG>HW?cRRo&RY%4A>?2Z4qD^_dI70xn3X-iZQk!|DIl(0H z{FPE=0Fl}h%yDn@{%*_N=iQ9hV|WDz<{W_; z{fFPdyHzYkznFL|OIAOx!EN?4>fW#K27WO2%zN4Qb}KP?)SPN&?zF+hBwGJJLW-sm zUp9-DHMTwthST@E4fNzqFxmud&RCHbzs%8haEI^iEWt)g0%69OydIZ`+16DwkkG|b zJ-?q(#W9QI6Ha8PA zYzm5cJA#C~38~%~GZOD39!^WUhG%1I{wQq88=wt?idm8IB=zm?#+9Gjk1wsx*rQ}V z%!JrI3AtqA{MIH)|29mQtEeyP9k;#6R~>!IXK#^9o;(Mv=B_WS_4`86IOT-j-No4n3Ahkze(SDunb2$mzGV4Ll!s^T*M30O!~} z)~BIGFnE~V;$u@1syShPmoY)Woz)i8W8aoj{Zp?+NBdVpEDFMbOWf?&K%f7xNCmct zn`SN6@7|zPG5y+3t#E?1Uj;>M&Huxl{4eJGhd9=lR;6K^I7W+z zt#XFemzP0ZRf?Gc_ZwNt=xWO3N0Yd9e!%z8EtH$G0pFv^#K|`hDbGE>=Cf%JPtUvO zY}WbghNE!d^TRFjN-=IaY5yMICT93U8alc-t6!A(&3^r!0=drwYkkh*2E`a-m?VZb z1~D%iuP<5jk<{OOBU=C}<3CW>2cGw4v(1 zRNSi{{iA^cf9cmTMm6Fr8@Y;6W*onemrT_)SH625&5F8~IIvNhVD0*x37GkGyokV^ z0}IPsb#JJv5&7_ue>VGx(aespnav_wiJkdwQOehU^*q%(pmS5(*a%eZrJyQSd@Bqy z;rlThK91dww3L@AoLg=SM0YxclkoI=wI3WLQ*Q4%94#(APvg}uu>ys|pt#qXcm|Hn z&ayhNyz$51%6qGijba7+fA$6Vmxk{7z(0Qc24#gX?fVhL?rM-x7M@TIyG&u`tbDAg zp_5qHF&ywY2WpmR-s++!m48@mN1T5C)#48%96kW2iG{sYKfGBPDY#--sF&9f-!b#1er((9z%(88d zB_h+&U*@F7xc{}hC&$IXZO`}-5>0&x-`P8ySEP45-o&cj(7=gC-pdJgwvRAmj4X+} zXMxpo^v~Bp=KGhfvZ?+U^1o)NA;7%uMPyav$_~M0>*+{3>6KI1rsbyW=;LDWm z$;~S%dHp2p$1Kqsf-i%zCwbE<^m&|&hm9C=qO0KnR>UfzIJu5;=|7ri`}#a@mPLNO z*o^WfG3)jL8&?0)xB8UMufLK)CJVidU>Yt^=7JIn=EmamC2bCs~ac3mBm=X~ zz{!aaSbsA|4?h39gw=V{iyrOMAjum-JghmQ_!K1*;tYiAu6 zDxnUjK9Bu$)``9FfJ$rwo01T4k!MP4 zI*+#!H}yalG4C!&XN^0&sHX%n)$XOX=X*_O6nzoQ;Z8<`Xx;0>qYvQ2-37|5t(H=G z(=b$3Rq)gO<1UwG@52Ej^{2oi(lWWWz8>=9hyCf<+4e?G`^TWp|Nn?D$g5LCVmfS_ zmd!ooA5WzPx<=7<63V`+n1;t8<-f97DybG?R`-XV_qpp0kj>(;siI#*6y%IM9Bnp* zHhNt}Xpw+p3qvk*uNKuHYhO9#JYM*Cl0!`(8L58SqNi|mHieo4#XO$=$3L?zK8YvZ zUEfNZ%{^TP=bJP4z`d^H(V1$VgU-?=4?y@1w_FEoxysgzFD+jO3|h&hrMkRP?qUUw zhBcL(vh(9_Z+rR!CTxQX;VIFqIgX$Xs%7&^>s9}5aIl`!qiC|Io+tAqzQRH|33N28S;KI zO3?z|uV5e?v-5{Z4i|k?4(~m^<=OEkw}-`9l_pzp9Ch6@hRw_o_&;sriX%;#HYaF* z?LZo=**ov9r@Le9jKHtoje_95pXr{-d?1hD19KHmr!T+b6!;Hp?L8~o%QQx*h6(rm z=rg&$!L`_aXhUH|T5?@#qX2Jt$dx6pU(oEoZj*O9r1}mHB}izR&)@V?d=4vZ-Mbm??Fm$o z2Mkg$Z;=uJ8`xCt^Drk{Ar>MIVc`3P?}RYt3Kbb*R{>u%In$%{^Y0&S{<<8_wmVUa zm=Ld>vk$ZJJb_DR%x#@U4-B(kPl@<`}wP3)=@u6}{>LEVs-g^2L<)FG?$ z=GYCNllmT6a;pH6bDkadZxU~n#sdZgR`T!mRaQNKarI=SV%#NWA&rlgGB=K-k755w zbm#1?ede2G5<^ZJ%)a5c2>*+v_H$T7foxrbdQ1QOocOHIahyU;bTvb%GAG9flA(_g zHP#uPko!1_Cz8gP^kEc3#sJab0LAh9LU0!?;z5v(Rcls9ql{-WqW6N|KqbOtrUY*} z*0O?g##p6#t7r|16{1rG9xTM)D|A>0B~LU;OV94VrVXqvz(zW=KU=Rid)%%YD6SZ4 zWvQR?@~?9RYp(#!q~dgpC^NLFg(#1rBvFNjcSwmi&@78?e64Qj_usLMB=_nXSkQBs z#pE7TLCg=L5E2WWI5v*=N#YvDqZDc8I4M32-FWm|CZ=gqi<~ z^}ok>g=0Nb#WvRw>q3=~NQJ_-(?2m==bRQYPyn^#*ev8pub{Zk6A^yW31jphK#|64xhVK!D6mirMf? zi!BrSArmW6YEvUdB08{PC&6nf`~j@Elg&kh!-ya~{oj=ngQDYrYA`_I)yI+b>uY>P zMerqjwT;cLSBg|sU@B;|9N9Zs+yyh!(NGelcDO)Wd<2+!I&BQmjgb{ClV)$>qx0E7 z5fylbhHNX^Jnp-@yO#%bc~@DwzF_QsL1otCTqipnp;KwrG#|I|KaS2aD$4Ke!at=! zx*H^5!I^k{FS0k(QEr&;R}62V5*>G0b_+z3;vE z1#_`gae(9i8d9mt_i$~4bY0X#^bQG$Jo-YafDcWEyns%;oysG~)z9LYC$c8UnH?2VDfOxj{96l@QdzuiO^Zc0>?AkV+zMlo z=r;oCKutTEHnz!R@3|D-5zUkXq_ws4rHaQ)<&tL7v6RMKZV)4xp`>-8QZDm*wW$G4fq&4zkg%1{0|dr))Hr% z{rWf8SZf>Q+1-t2drcyeDVniHf0GuV%7ZSC|I8jyEJinefTbzF*HsK%!{Et453{>5 zZm^4HHkgwWKkt43s=q+)b>5>B-=(X0q(nt&Fsk zv?b!#8dAFZkZPozthKp;lLU5oG}TlJ_S$KgVrFY;q%7`lX8wF>TY`)p^B0pIiXuQwEb#C+i$3jul1xofC%diGBup{lfJkNQkb>>AySXiDCtIuOT z4FHy&)W$pwn1=oMkag`O&mva_d`ESZ%U0NU>hIHUsx-DwlW@Nly);ymqK;PQvaF`g zuBC@a#zGI?JBO}`4ttY#ySTZ?&8d!Un^qtP-ML@8)my@+hX9r#5}8NQN@?4ylh^TvV1xgU_w} zq`~Ck=TQ;GtnT_KVX69C^arz|+WRy;ef9+oB zl|B=v?LI$ropln*p3H1olA`Or%ChT#>XX7#aWfY(P4IX}#!_zrqR3GVGr-)i>0eH~ zxU>X9Suz{UjmL*Ml?f$m7Bf z)=AxIHZkb@a~PT|0-dQ%KLBb%xTefaWDGuicdkX#k_!+g5&J*9F8sd-K@<*R^-G{V ztAf5l&#=9^P3nzm{7%-MXwjVh-Pchpq5dzHYj^+LKHgOkz?i@wO!l!z$D7{2K01l-_s6d-;&w(};gW&L?RK zFiKx}v(!w|Dur>IM#lHdngyM5Zw~KIljLO>+=2wjUIF)^N>;5xCMzG3h-j=?7Mn?( zSGx%pG>0Fgu6%!c)pJT1=!>t_9oJml9_OXiEc7FhO32KSjqg46#8iyHF)O)Lv~V~D z2i=!hi_?l9i@i0PVCP%sylkn@JZAL_q7;p=ZI|F@6!6z!6Y~#{uEQsbWFedbIKkoi zIK#fvqs4P?2qDy%LInp5)9JPvDW%m0IP-BV={c33lsD6bR$lLj#f`*2SN+@=KNr+{ zy%Rl>kP^>phm$HQi}ikruTV`a_?Ao)$)*z4akXg9WFx8Y}rM7CJ6dMkDnIZr9kpLCOF#l@t8kXmwTC)fD5nP#XJ6E?B3 zXW*n~`lclkGULAZlXmLx!of!Ze)y-pqh%7v-yKU!&s^V3Dah)Pu(6K_3Np5R@zMSH zM@4E<1hWepVP7patf2K;H(H=H-9JLS871AuUn8UJRfDVxvN^#((C?f}FkH0k_scgf zsG2_`r8*Mr-7FL1m`Iz`F`?wM`x$A@`e8|Tx-<_x01^$;&JiEMpy_JXb z(xAwR`1a|(X`eZ&c=nL+`pGow&hN9>syIfGe*Oc6fq*uEHQ5xEUh0Su>f@=>Q-N`C zZJ+%`;(*^C_T;?SdYzO4U}XX05m!gE@a%>&i@>FKXkS=WaRdMMMqo&St`zt7qG>$Q z0Ji}zX=Y5*^!gK&%p{PZxsncw+{V;DC7QfC=|^`odcDPR4y@Z=DjDUi1!MZIUr$H) z59YAh-p}(x^C+r#(YLzt>K3a-Y@jLtb-u8l%F;$C&dNSkwjUj7OR4EomYrB(*tY?M zldHs|=iE`YvMqDP#^jxiT<u;m}Aoeg{-=SE8Bh#eIeg540y&6$eYJ_ZwM?4wx^sRbngG z&VHwE2Uvc`D$3^F0cuV(xw*_P_@#o%&&hMlAbeHBbbkOSmGjcG#9}J{OS`$Ndl5y! zSYN7;k^8Q{nooK1V(6t;I3F`Fhkh<<5%30$ICSp(cABtaWWz43GdE*c4SbZjZQv#) z67-PIy-(obFM*Cd#c7nZPkS>SGLn+sZqa5oyNcZSe$PY~dd+yhOxx7>EtaN84X%>P zjBdUwzSH|jCBi6;743PPHi36>7r}u0C>&xd*=-4G!e!MnehA>IXHfIir4vQIGNP;9 zBtjpu&e}yAD;u#LcPh*~;AsA5YII0e+eGNy5S=Qi4pVyp>?NsO8n4S5Soiw2f_9?B zw#s%b0`acsq(4qOYGtJ+)z10(Yt_FOVTJWQgPod^tLA29IecL@s*!c3S%O$DH7C@{ z$I{wv21X-uS>MCz9o3uOD{o{uAS}C#8nVX0K6d-}T{v@M14mk=w(d^yOT(dO$D)5$ zkrmTU;e5!l(dLE^{SJ~=LOI$Q<$|eS^5tM;amaK(L~jO>5_@?GAT`vRagEH7XgNv_ zE5gWXwZYa}Z_ejqr5rYv?TAwYgHhdyDZZ(_gi{TnT$+JcU#bw5I#oi(`%fO@K?*-( zi#aAVOujlb2n89b7Qq#!9HxIaIeOoE1(-@d?4exZT{?VO@@hA|;~Thuml6PaIB}hU za#6#laW*sdi2qR1(@8E6%)j|v_4uKnmAYXNZ5>C)OdS{%G{=i$HouAGztBYeHCAZ(D`9q&vhIT7~0l)GQ{U!C0(l6-#59iOK z{=lT;S;rR2XP0}f%a#JBCv)fKh{Vh$30e)_?^)ctqG6NU+rs_R$>yOCptyG$9w|Cw z2g!E#bXj|Dxn|uF8+uw2+IveM^!3DUAfT)FJUHD8(6}T)t7#Bf9A$0A_1@-{W0PdD zgL-yZDYUOEQ0dPyvPfo+YRhh|s8qcU=s8CztYYAVGJ#X9R=;i@TrLSDfytVci#D(S zb*h095iffmuLO5r)VLByGiuQg*DMTZ$rnHyC`Yp8reRlw?A zeT7L~!yPM^Y{vqto&FF-2GPF3JWSppc!7qQY3KZwhjyss-DZnO>HEG6bq!6|WaA~7 zy9TX)bE*HRqMy6izWJV5N$Sz~UFU~QRLb_7!#Ahb_g8zauIuP@WuFK}EAK<1O^viR z*PR|qwOjJEiMy+~-m{SQ37l_KsUlQO#&>nSeHohgKjt>LaF5mLNTa4Qb50z+He?%S znea$^C?Y}Jq)U!993)Q*@n?Lf6pHp&btX&0TBykUroD*;+5+Fj=0tG12a}IwTqnF$ zqL+kt{>>4ooL0m_ug*|SWv;=8Vw&KOhD)kuB`8&A;&|DjD%omHE;#okria@t`n|YL zZuUpf7Y_T(r5eli0{IGj8mXOC^cvnnnBDtWV`0PmT6uityzkG`6{`kqq_@v@azMv_ zPHy%1T!5)BhD&AROYA2KvMJg82ZJ}1h>`R;D@SDs+c2KWc1#fWj>i2A-|Vfy`Ep8Z zr2J$$03_PJY9+Eu_K~=&A_0|5%LzVQ)bb@EIh=V+`8+-r+XV|8mo*x3B^|z>o5}%H ze*bFk^7h(NxoWm(`WeUoLDVu-AX!Pq5)xxs`lPoo88BH0 z2FmNSs>u?c>7)M5JvPc!7C}r3MV+;vNvUMJ{ava(Z|S)OY8U0ZCW5E7)Yn~`t?55e zlc|fcVZzrTaVo|RBv7N2%%_YlrCtt^+!`yLwvgPkGRj=GayHIXA^R^Y$>r=34-kLY zYh6-*Eyl>_+}I{E4D2e=n}l75+~~#Sbm zMe4IQb9mKCX_{t7nD3Lu{Bz1NvNiG5u2`-0>Giri-sPp+pZ`?ZO!hqkLPi~=U$v6u zGVg3{#FO)ypN&n1PQ)4Tib=`XcR>~<7TiI#;rfNJ$O6-R6>d$rRu#4*Kb@w3V31k? zX{7ASl(OjTyfW0<{^b&hKS$5@E{rj&th#bQmLh?{ zHWsELK+7#EC2ijozuzP-6hS7kNgeyodn68fEcMzI*Xj6qOyUcX9vfD}- zmD3`(!C}iL-i1dWbV64(nA+l+Bqk-jeSJMZ(p@7fMK?a~zbk%i86gVwJH1dX)oqe6 z?W_Nv9Ttg1ZvXugzU`qv00F6guMYGILp=($GFyEom7h%=v_qQ!`T;spBm$R!0-K~L zxOMhBRun3_*b_mGD%za%8seCjwuRPC)EVTRSi&Wq4>sn%2EA#6Ru$OJf5j^@5#uy# ze47cEz=%;q{Q)Qb_r%!P`0l%Jk7k7+Ln2s`+4GB7pD31$4Q?kF-fd-?m}|m#tc%qZ z3$lexD15N`YuB+1$Q00_+OJ8TvP;1`!nB{vob{ef|14o+_U9N8kfkzyMTjYv{*3S^ zOG1${sV!@EJk$3{*7z7_lL{!)9$1(~NQ&Pf=sCxpyP=@`tGVLrOVu^X#T%P394-Fj zQ7;s6_$0+`1y+<9k>jw#PbES zHL}(1ye+3in-yR-dzCLUqr_UhrDm!m#u@lFbA77RD;Ag>gtErVFA^rNm+!uoEIUzd z+nVj_6sO1~zrgArQ!QU$f^${R)O%Jn_IWP*4v>xM|2aBr=xR~T$W`VOe9GxAg~8El z)yABl3tfaaB3hg;0~E~S3OT^-OCXoX#i~)Lw}Aw3akjmN zJfvCPTbGyc>wQUl!=pSpEc=JSyMJabXT*&O>79jZCY$ywn|S{iR{{aei< zRF+6SyfVa-6W{vm|AuFh7ereWF%@7Al{0<;6AyYbaN*N%bm*ItP_rpHf9d7}VitD9 zAkETw>+M>~`|KG-=BD?MXUzeCm$|=hEJSLU^QJ|iV`+WkEPUEVt`^7V$`?~IW}(j9 zbN9P+f0c72L8-e+2XgCv7#}MriE9k^D-(BpJ*8&!r&wd;`ts@ozG(-t+fe_33!ZSL|m#T>oN2vfuSeC(sz6JzLD9? z5GT!HpZ|%Ke~^O3Y^<@6YJKPb)l1PLa=U5Nb{4>hPQVPFgLuD@R%_|wNcYHMp7xS*efp?QSEVtP?n^sZmrX_8)O znrjc_XjF`WMG&|(_7F}TUt#sL`s-U^QBUF!$nh7}QJ5uVC6-6ZD!0rHHV1-bhOt!ft*#{%FEL3hix`>LsWF- z47yhhntzd_w>Y?lo$YlEM_PW{M^Y9XSa;yToUEF}C&-%9cg}&>UyxS8ex6I{TF_(T zAspFRs^Zzm_Iw3i&v!T9IgYiYPS^EFG3opH(~5UYdq}HymoK+Aajgp{v}KtUSq&}5 z?H}X(qyo2%6tdNOj+(BJSpO`+#yK#TOMP74EF=1;P%#>!Lz$x8Wa94aZ)x>IjcvsB zjP65%E@Y+|iCnU^cg^IQOdfsB!(|&XY1LrS^^31igL3&7AMG*$_X7H6j20u6%}3@j zk6A33-LXN29sS~)F9cxM8&{+|YADpCwWnu05&$M~?Z{*XwfIDq3CzKZpg#SyE18FP zxA@1it5kf8lvBTb-6Y9Xk&02(BnAr$1MW^-Q159kDSDU^CvJ1InutTTW$P56BI|r$ z-&_~AC9`xSMq}mngn9m!^HcqMbBH%ni#uV##?Z3*2B$3jEWdRavwwV2t191nBIUyj zn}v6FKaxinr8Kx(Op2GP=PuzB~w>l^alc1E7s^x6nmOB=5# z=gy|-5Ri4#Q!qpjw3=C*Bq;O(m6f#bQ`d($4^s}86z6)A+#9RG{wq2l-te0D$olVy zag63^v;g3|4RM{D$%uvX%Z?>GAu3PdPP_a80D%@?#_M1IBGy_Jdpdzygr=2l1;Yac zlhY15TVfgYS(A34e1~X1a_m$XrpH&HPN_a*qwqbTt9W^zZx8>|XzZL-LTW5{q6S^} zD(RMG+ue;6KJWuOwi3>{$rXmw&k0r~-y@K;kW1ea=qF-HTIm>G&o_I;k7S=B0&~T*Onx_XnP03O3u^m%j*?SD7Dk?G&#KdcgwJ478OqII=9a<~ z^OzkzF7GFoQy!>~`w7@eW+UDcwJ*EG${|c#7U2Sw*nRa0^Wl@B7r#0&?pY&xKPK|U zY0dOr$-|eF*faAFQPA_B)Y48p%iVh#kpJZV0g~&jm=QJ~D}?dCr$eTsfx{tz5<{e> zUK`t(z3UYnK7Gimj;?;Y{Os?79ELnPtZBj})3K)xgMLkRLPmmgO5^2NWB%WrV9A5n zCtCW%EV*pcrK6@hY3ciHb`9UM;+Yf|R5K9qRR}_Idl8zRHo)DyCc0T$n+!4eFceds z-iPzlWL1eRstna%&*Bym=zDo{LQQ|NcPjt0i%c6@>e2ueVfI(i&Wb=@#+TB8i*sM` zH=NN*A#2Xb5;=q+4^V+ka9y*$+ktYQ^otskm)YA}pVwosP33Gq$>^1hH2%$R@;hF` zQ&RFp%80J5og@p5`JbL01PFb2-o)?>Mf?!b-1wU8Ix)eS6V6C!W^w|mRUrtp;+c1y z#os)Ny+Pnem%>t~W69Drv{0{!AH%}*gm$}g)3SEKIaRdmn+M!+dcO(Ay8{H)T3(*< zHwb~u6iH4tm2(D8$7#5*qp#VC4<;QxwbZxT-Cb{~`-wDoCcC)Tv$!9XLsN+uLf3u&I{X zP9OhG_O9J`d67pBP?c0jA1$Ewy)XVoagLIfv>&4$uJ~?~a(#Xo;|d&8B}5+>u^rFN z7tRTqUpe&pNn0b{f7RYe&LpbRhe_0`_bd!nmNYtr5UEhKY{P|YzGM-%CO)AseCkMv zS~Oq&c9l%>U^#2f&+jwS_fc_|t>(Zcr{vz6*ni8%{5!C7AjEl09cJlcmWe= zJR&PO#Z}Pr*xgI^sgT#oabV`=Hd(L?^o| zReZxbkpZY2n$S%Zl}_>P>q9Z%KifvmE=Y!6W-Qcw7Zs!P0RR2Z zX=o6cl{QPg(%ro_ae&kP@Go*#bM@@c2sNm<=DnHYEj&RKhV<5jUZnHNgnq9JxrpQq zxewo+TsgTwfTR&<)=%@~;i*kdI=Z__>V4(q=#G>Ad5UbYJHd7?dmyQ{liqvY;<}C+oo0e`3GOZmFncEb4u7jL*O*PKWP%O6L9gD{`(`w|Q^gkaPViyQ&l)NG9(6~UyujgaGTM_Sy z`8xEqX5GE+y^Eesc^SE!3%8KWXzS_@gHX7 z=G;Ftf2iEmal`6R9Z(%Jo`ich0WWU2Z3-z`fvYseMNjAK(GRb3?1g6c^u8I;2vwiW zo24<-l=DO(hoSQ6aO!n!L~UGA{kXERGlBO#4Cw4F> zDtX55&Sc@wENA>XId&WFz0Xsajs+NuE|G^>-zDMGOUR;*FLA4Ifr7-8MD>a#>&Fv3 z$CNZZ7PGzSqvm9lsUy_tdFP!mv5wo0rgqaN7DI)D0N*N*^BlwvA{%1?wT9c`mhFoS zrRo`a3~F3?y5YOL-CMgdk68Wtq?4=CFGh$#ixz$8ruETE*VueBWfhr%h8cyq7e%C< z!gH?Ti5vr0GaX&DF0+`pF;!ru=tCmK{LM~IPOX7ibF*dG>!*6q5H{UQdXjruv>Cbk3)+a%G{!?nSWDhj z(!FO?^C`8ahByEne^e0$1Uw_^19=KmA!=MuE2J$3R`EbQqUxQyz)ib(hKpK&mGwqM zu06kbM1h2A0knfvfAhcHU_qRsuKLtFb;KOEmg^8a9m@b_i^QX4c{XUy?zbG)6jm%) zYD%V<770J4=DQM1M`(`aH;btkFwh824y09JeCADD}AEW_!C{#q>q+oh5*LAAdL zl=ns#&S@2tA_fb4#RC~Li*MmfS&dGwJIhEWeBy4e=(Fl7Sloh9oa?y+sqzyi@DSgy znS$i3RXsm0sW84}R)3#1Kwtq!@d3LKaaPi@rTzI1n+uKcIR^-6I)uBpBD%gP5Jdkn z$%Vg!0TQ3YWo6|_e{$0}a%mOC@*~*?NTr}Hbwe^eDv83Nm8GN}Op73Lz2hRR46Un~ zkPs6U#0)s7P4%x6)rk~L%uQz;g=e&Ts!6Qp~9lsFC~Kdbb}+ zyGjf$eV-l3$}VOLQB@DTwbqJ@)a^1p3#PBq;4hjH?pgRF6cAw=)YE(6HyTIos+=0m z%$;2)LQ=+{ArM|4=CTwGw}O?zWD(2Y4&Go`^SLQ1ne`Jbd&f^}YzJsRsjSbM+r2w=qc_FX?7%tk#8@IYmD#?*q~7E^ zc%bWdfI#pr@_hG~n4}eLnTc7VA(u69%yX;63hd4;+27?pE`$zyxlgc~&isL#QqvU% zUs8gDq?7FgB?(Mr^SM;IR!}SWDG&EhJCX z4wKjpZVo3QmzK8g&hmD7%?MncnX*mdPPVZ+2ktK+kE zj8%)NRS|s&ZZV0rawMFosgw(CTD*koC6s*Cabxj`T!&DEwB8$cAo~j#q-QfEs+69{ zcBl>G4E3YUj>MiLoTC?Xz9;gY{9ul)N%PZ-h5sQuae2*YJ>5M9&&k30@|Oj`E=)|# zJVOFT25->PPU9z7S=9v~z|`y1Z|C=BTfJeJx4T{P=Q)3ly%9sv|W$rQFz&%s5I zs1WKJgD-uPb$cxH9F9&>Tskf&DBk$eL9ke$!I1s@CU0}rQ@5s`b1b&F?^kTH!;{VZ zbc116*dU?O)lqTT^I=S}GfAa)RQfKKZyv$8CSLlhnmvuQY9vb%MjTM!KNUN3y%YXc zziu)@>DUv5%gsYhS!T;*H*FPU>qOS}*6@J)kL!IyWvV{YD{`^8)AYx`3y)F1AiR$jb3PDV_O@h645ESXDv8T4Fxw9ot5 z=IP(NaW6mZ)Kakm^4CtDNVfC%keJE3mmqz4{;R@_Ome|I4eviQ7_Kz!|u5 z!P<{S@?lmp^~GBw*KPr#P_f7<@Kh_O$C<-p?Ob;LMpRl zz~8qa6>+8*n0J3FdVrbxd~I4Dau5lWU3bCGK2Me;I;pkwlE6;C)9~eG(@O1=!MbjX zFNTO^SNGN>%PBN-jqCl@dtaKvj;5vX!$b6veNy1iVD|6Z-ew_X8N0|)2OPh1ajAg6 z?;3xUx({iFoDmnu+){l;rSHCy#;2Fw0^=7Z%l`__@MF}5tasA*yYzEiL(YlY^E0pn z>V*sp$d#Am*hT_K8%*jj`;(9F*8*E>$2{7AgP1ut{PFVfvhHSIGbDPm z^=@6}A7kjLfy~B%fjOvWIbYz)bOs`f%1=0i+C=8#>^?}Xp5KrRe*wYe7k^DQY~1#9g*xuRBIjLx@4h?{*bU#py6UZ>}tViqxD zU|>XArge2m0jeDoKII3#YtT!UAnO81o#}0aoltNN#*3=dbHxk>4T`HkiPg3Gg1VU} zuwT|gtu`*uYbK1!zk!14if{2Bc`@Zn-0I?|X5DEJDrt~*Au0^hY`D>|XFZoP#F=aO zsS8my@J3>@HMA4K>14+#7!VPGa_=oyFrg4?C`pOdGz^ zH!^!FS-ON0Zi`})Yp39pxSB>W>Vjy|S+j?g7gu7VUWC2=VCR)Z4Ot(YOWyW)uUkMU zZwNWK$lPvfkjSE{t6a7^t?`mW~pC0RS`7V^7!K8-wcUJ_j!XE<3!Rr@w)&D ze})mFfcJV^<}N<>fKkCo;JA`Vad#}%GwD`m0? zw-3l=j)js}X|_mL-72{(9s~AyE!`gWgyn%sZfVk?r`mJ*s23||Epg@ZR23^JD`c-b z3c44YUE0-+Q%Wf%I+R{Pzo%93-kj}oW>iIr&bEdStG567b2v@?OwHbD4g-x0sx{?! zHOu5PeX_J_)%^Z_T&zzh_2hY$dhN|1Zz!OaZJFQg zDZ!6vPtpqSp8=Ri-!MMi3_NO&DCB0$e8v&K)zeG43F?2oBb;ep;n!{D>zk`7eUF|t zf~u-4-X(1w^pHL_=KQnftjdw^y7{Kj|5*3*GjEh19*!TQM{EWr{9e&0 zNd`O;f-R>*6E4Qwd2l$x+Rb@gwi;_^aNzjd9I0N>zHfWbf${3bRmkyGJzPErP%jP4 zdu+N)P9`*^6*9$K&ARN`@@5Y%zRr}w#vKGoxDqP7Lqjw1r61l32}}1O;R+=7UOC3& zFK}om&qN;YLpLtYMZhN!;Csf8!Al`zA?$hBz!_}>r%B4L;@BDnZa^1ZZ3#tELZ&e0 zQ!)g*p5FAakJ|6(3Q#i5En;Aa#XnMsR}K^Pgz{WPUH!)5j-dgjEEq%iMwu_1uU&QX%1wM@dm7dw>3l z2KW_rNnoJXpwBcnz@Z9J^{={6I_^W`$4#8Q9F^I%k=6Q~smx4?)|JIpZH9cElwcKY z=nBDPZ@7QUJgF{}Zp~CS@|tUGCZ3@R+cK<%IZ;Tq{ZK#64B@yZ_+cbds8+^J62GE+ z(8j-a+acb=1Og2wM{WAa9iH=V^Cz~F3fB`Dk*B-Q_lu~oiCvgt%I>$H23e%&=u`B6 zkt<8G;;DH=#z@g_6bF-qFmZnyKWidg$ps~Ax`D$Q?xtEpo}E|FSBsb_i3ywjts!se z(VHZ2kEB{PODKt&b0QN6c==vh40|4Y6G&>T21X%*8j-!3&P#O zB3FkmVsT)BP1`6hNgFC@}=hNS%HU z!nRsePHOp|bVyl5`l(HZ<3EfQTQy_}(k)vU6PfDM(9x`8;3}p8@Jr@4Yw;(csOU~U zn^KLX7wYqB_9C5+*H7$SSI+>srhFDH?k1YS;q34@DD&2ro)TMcs+dhUlYAlQp{g!R zbHR|Uw0`=7LI=`1^f+0={%e2>39?3!eAvUWB|wOp_N>XqPndPZCcP}`Smd+|lJV@_ zJOc-Q5pA*e_tRWeNN%c7^tR;Y#W}HAaPaqwZD0qNEzrMyLyVI-!Mb^UomUk{T{&9L z!Hvq}kFLP_t|YH&jKBYY^1y!p-f1P2*9a?9d*N)>Bf0(CW5B??b8T$0@ci$HD)`F! z!LIA|?}f?he@J5rtM(zb1Cez!ZRr+yO-;H62~LlP;>)U!1qT)SAar?J~y ztUuFjKHmhAh^4o^%!J4RNSCGr4IjmyhB?YLi6;`q7|;H7fS0$5gGKEU$)f(s5SSF3 zPoxUJy4DLXmd!cvQ>!PB%9Y#<1OM^$xyTSGh=z;Ow zNlfTB{Lt%|yETlSe*41UxQ?r%`j(D~;nnNpCUm06|xA~ z+=vJl>Dxa#M}J%!5X%aNEC+M(BYxk41gzT3_;NL0zMYSHA5|?d${{!_fj#4u@bN#> zWLwTrf!=0WBT|1t!ZJ?~o2cZl{*vS2<1-GI?mK4=yp?K3S4TJIzI%CkHYYe>-*X!} zlW&&U9~c9kYTA3LdQH8!CIJ$`&CUNXy;Wt|>zzX_U|CY>3G94e%JKB<_iD5e$gVdV z9Jdoxae1?hmZSZ4yS3D|*CMd0+mu=N1Jc-W+;Ci<+JSi+sku*oh5ALZGbSdF?d;B>E1B#s)=!8R~rdlH8PQunFYG+aHK zYBnUJ^t#r$^x;4_q^kE}nsGUwyL2~Mk0oS^Z(PT5y`3lHwT(hABD2o0hKcGB??d#% zB^kKC4>U8s7_I|kk=6qB)PXI34Vb=z){Nt=WI#}%Bs3+Xfdw>gejvAj#nzDcrwa5? zo?jSf!Zdeuq0tp^HqJEf&%z;7&!L|GB<%8PB@0aqA`e*|3v_;PY>R5Nb+>Iw7Hgv6urjgGd05tx> z$1VC5B>U(ul0-?))2H+jZorhDc^v$Rp{uRKFjZVUgZ^BmkjIdV3;oUW)&BptVnAjU80$8fPTyk@PJyCdzsY&))TzIh>be3Ytb3(WD zL7zlHy`(bO0V+qLfXRp%{vT}f_N!ZrtAyt7uF6je(k+7*KBdlYzT^Gy&oZNUUYbPj z^OEgh`SG^S|Kc+JBeLWNS#IkqG3goM<@+v-tNjafWp)Q4s>nmKQRiekwC4IY@s6zp znS0^x!?li`11kwSD9Csj-SrmUvCCG-8$C%x-e!)3Ko9A_H;-LxiEdJRD1frk*@f06ek}L(X`Lfjj z^I`q$U+nq0Clj5gnz#au0wDhUZ<=lP@xjbxEHFq~7|Xi}(nw*kfsS5mlPb_s+jrnO z7Vs6;unQtKM^%aOg|jCK`r_L+MQz0ZRz2s!Dq5<Y7+&chd1sMMv|hDy0%akxi@`jm!)2 zJ|D^S12ETiQ-jdac-E@mf6rtI$BMTUk>N4i3#|ol-%#%ZwvG(@?sLqA2yZCVL}N|->Cq;@H^|Yw0$I< zHYS)LVMbHGfpgr5&DOFb)~qM);3qIG>6h&l+aO@>?*2pj`}`n9*Cl8X7)WiqWMY<| zV!#sE;d^qQT&0>u1STjK)hQPG;J=J9F-mtGp;yNQkF$>#J#&^6L=cq7Cgr$i|H=Ht z%z1JjvOsapEvse2+SN^__}2fwvUCS7z(Sua=-N8F1D-J#JghNcC#jkd?$U# z`>(YvmSU^9zTtRA_?i^HsTj<^M@jT)flVroM;lZRi&T>f&dj9>Ot80sm< z0d3FjB zF-t2hEaTfDvu5E+uSCULlHB~nCKE09BEGKhA+{VNMuJft|0%4PlbE(5z3s1_@Yk2l z3CZZ)-6nMjUpa+c|L{FqB^|Gt}HfBE~~LLn~I%ik+G(t|ZX+f79e34z4g8FLo(4fDI6(>J4Z<#uW0= zPwqw94IE!T8TFLqNL&dWUsgIM&LoAFIewvL<1p(A7$i5B?!A-;IJ-v{IpqJU8imwP zX)}{#9+G_!{dJ-D&IpX(-q6(ebFi@L39=Kn-#@wSxeWxf%n$=d%Kax-PMP}({1#^ciVqJBoh>a5TQN^43rS975m7`I|(K7{H!tCsb8Pz z-OE0bO0wXsK3`P(uJ|2W0;Wk4b1aCy=T1b=1%;J8gS`H!NNP%!Bg9kR#4v*EqN*(T zUG=pe$nt5pnWUHlU z1GD$y%CcpkeGJ22tuM;lvGcDEaP_ith1hJ%Wpu?NLgM(Iedd^OklejH%Ng;KHOg9m z;;?;Dgk@N+DwbQ+W>P=oJ{~+qO*=$ZF+bisZkeA>90#c&NUfG;2C4@MwMf$D>1HK0 zv*TSCfBv4`Z_ZX5&t5#Zc&VFLSg4~HMX1WkZeU;l#vD5YTJARb!YT5%>nyyP!V80D zrA@VB)wE^kK6jMBZpQnF~7ob1NlLQC@LDK9!lDd~-%ko_p;1i7~1y z3L{H_{f}SgH(5yVGAN59H|ie$)e(ZK?0d~7F$SmS@6mqDIDxR!)1)_b5eK<3HQz!# z-8hyYEfvKKDuMu+V19STnGuJkPm%N6pv1F(QtcDyh$_#FO=_}D#$05cR~vI2(;2gHoc&mF#~1ku5sMGyUM+D)<8=VKGErvygGeCee>c1T$W6t1^W z>1HrsQl;ww2S=5!Su6s9!y1P+gF9uY=fvI8EwSIvh|0FkyOBLBXW{2oKqw&C1jv^QGKi*7g zyV_$!3s=F;$@l5%5u%-S69sXlG7!n37f{SyXil1Cd2tM!lDm9zpmq6l<wK#D!~LZH<+ef79RUjq%;+um2;uc18GZ19#DJ|r zB`283x|v`+nRj>OJy!WPcWE;Bf^^dV-h!*gn>TMh#inZZ+~76`1c#iZ6|UV%10l7# zB=Xd&Z&Lb^^d}K&?PQDeeYx1()YfzBD~rPOG3tkh7msf%Oh#?gd)Tkndfz^txIR*+ zUa%zoN`AqrSk2*?%G_7ekgSj~f8r*u#1Z>cD;o#*=krO=pEc-OL`B@PW_HKo>5W_s zANpo`vY(39nv?$^2@|8pEnugB5)qbxP>^h!UHnly-BT!m5N1E?<~jgt*|WA90&ZX7 zy$)3uo)JZnDuobc4sL0+jxWbK4fupUoh$*p6@yxAJB@5^ne3HB2Sd-a(792qr~I(xMV5BuC9v zS^_s&D(28JSx_{@Gh1ZfRY~n+fO_T}qWkO3cOdiToCdABcD{!Q{&CpS^xdhq#HXso@4+bR|Ni6Y1wncUhSg5n1@v6-dsGmNpUz zy7<=pfh>B*IANDI=M>?xq*md%(k)reg*H2#x9&EO-z9N3EpwwI-o^8QZ#vR8TW3Ff z(CK@Sbjc^RuFA@AI|Q`H9u;nzAyCLmndAx@fJvenWTR!%z|dhy(YV5;Otk6d@P#TJ zHq!HW^+AL>PJmq)Qs41mE+2*Wwc& zShKk2KIa#EUmKg2#&WYXHyaXP#EG|6A-HW0QEWyE+%eW+=W42{6SbKE9~}TWB|Gc* zl8@0#UtJYmrq+znCe6NzwtiN2;#OX(hXaYdDR<~hqMRgmh*lEkUD9j0dlYFzfjakk z`@i{f&c{yHhn=nY!(T3GO!U^co=o~zHNkVo-N=!Tb7zMtS<*3VRi_~(iykEzZ!tsX zOUN*=@Jn(9z)u(IuKVg*8jN0cDB8!I09($8=4Tmots$)-aWlJgK@k5$q{; zMVDy%8x?mJFV8aVip7dOAlQyRxE6~1pXKn4Uk=@dW2>~p1TX5XCHim$a=%e}VGBVh zv-M>4-)qpn-Qhm$PyS>32l77>`fjIEuWS0QpT4*~5C3j`KWKeM@#6Mr_`~Fbc=#E| zzo-`vB@Aa>tN+l#|0;*axk+~4-il}b;H zGfyx#6}*mt=YPZ<-`kBQIH5RT?Ivm)PYCMmnaYOz`~4sgLwK3nbZGJhmc(J2)u)IJ zmANVSqTSeJ*rmQzcjyo*Ju7L41*DUHJ@`D?L1iguo}_Xf*R3*|n89HkrbRqxV)8TG zb714@h=ju}vxdvGmwj?x2u0r``&J40#j9x1yU{@@rw-aG-Q7Rr#a5T1T|qPbJFvYs zx<%L)`2`ndc8IC?;r#j@IM!d~{x!FpI=;F-TimfoWqs;(lA@Y|wYK|s_5(R2cK82mO_PRM&{yq$9KRz2KF+(qUWb5JPuSQv zI9|P5eJJ_2KOB~Sp_`$kQvyRg5?&i5UOm?tOL8pJQz@(D9!bc{;^3hDSfI~@M`ne! z&RL(Xl~LqiGh_sePZU(Pzw|e$b`uT{WdaEPboh9_teJW_FUg{AEJVZPSdh;4bbK!< zutzR80MpR+!DHGB7!{$J#XPuuQv?1|U!8KT`ce-TNTCUEay>T~v@vf|?1lK3_q-7l z(}#a5xlhn1`)NuC*WQK*NoxAoVDdV;|DBg(DU0WBevh4&&zs5KsxT{_sJ3<^vup(j zuGtzxtf~oYpnqb1KL95O4IHrJ6gV>$j^O(f)NJ3)G+Ned!N*BRMquV+4IYWpzEq0w zEp$-^EuS%e)6+6q#L8u1IWo~IT5)_(8MZg82sioq_a<5C%?>qbo+I`&^#n(mozw^Z z{O_DKw{1KaMLR6~9XX2cysl3Er_7b%DE6UEs6f+_67T1U_sGe9&Uw228N^w@`psO1 zX-Z4kQ&sE;dz280XMIM4|92?IyZSv%G0f5}+ylzdT|v{2h;oaZeB{vwu}?u{gJIh$!?CLa}+_r|ERJr59&L|WZP zKdTTq>UB>)udD~;SvuCT+CG}Rl3g-bD5So?d?FT>s3%rgm~2GvQ6k?p&+&<@-7L3T zn~5KTw#t(>O9;@538^esvlGoT1#`cd@ND~CcPVC;2i89!6QbdfSgz;#H7%IQ=8ki} zc#1vYlz6ool3M%?fb3aEW9q_Em56{HNI!!VSk%&-s2=m1^KuF4l%bx%Ov7ay25y}iF%abx2&Bvj+ z$FFyuhXHUA7=@m{?)|rXe7?u=@oZ(|>bfHQR{MG?HEiI^7drZt@cw|lJ8AkCfdk6N zeUo>91oyV<>iWOE+A>4v$K%jD$-m!^LsLQf7Ny;L%Eb_V0rp&f_I%AQZh=af*A_AC zL)PZTq@b9|K5&XN9yz)jWBu=zLg$0j(EVF!LAqu$?YQE{nU;kaRF~#P@f)fToIiis zcKvhP>ouFH-i(;mIXevIEdGv}bsBOB9rPAvOECt2$RWBxjaHe+om^owrZ(=w_n{;u zd5o>jEr7{BObShc=<~vY>o-df@cB_pO1^geBR)Y0REO(hssJZ`GUu z&Z4JZi>K&xToQ}XMPYhOqcsU0uU>veMl`XWNc-m48mUE=84whC8;%@S0`y) zuU4wf_eBineptTL?nINPdW5G$iO26GUZ{*`gNe74E${}-$=wmN#V=sC?wR0udBRAX zCr^q|pON`-4`uC(V#-oeGqaqok3J90S7xhMy^|~|>Ddr1&(9Gbw|vL8M#ag|)y*0% z4UE1P>ewuh2bp?#h7KI9$v{8oV=HnA{q81(g*1|X4hSGs#QwTa@TS#1Lrs3veY}{n zHNSiWS1G_=+Eu|$MV~__x__y<=TD409#d7c@HDG^kJ)oN?d)aE_MTp_8cSEg{wx|$ z*rKP-=xLcKH4(02NfTqF_V|)o70y&zTFd>?W>I9wpHI+QYqXjcPa4yomXOKsq}uA! zTj;Mq`U9ad!Q{FwVGce)qZ;vYwATKTrPGY)W*MqxI?uSFrMTIfvRe@q^H-K8)6B%<07)}#J7JJl(v{uR;7cRLFyiXSMxIqUN< zQ=m_JH)bf1-R_uwdK|~IKY7~rdAWG~?01q@#pudI8lx}gh)Q$Zau1qe=GpcoE^x`u z9yEN{_tt&4DGpRo$VSsza^Dah>Iv%eQ4H?rooI(3MYxlOv%QA?+7cOIN`Tk5gwz+o z{msLK*XJL@Mt43+tyNh6%?>{~w>~)rXnFR3zjpe5olh2=>~SzmK>>*2h<`U=&tDSn zjt=uJ?~~XkzmJargL1rZwF2epb9|k)vB7ft-${_ZyiDJkqpa(sVp?qQed1C)@g{0K z7k7J{i(fu>dJMa1o5D!m#s*s3ldP=c_6}*=#&$%LEt#gS(wE#TKFUcCPQ#(YWwOFu zg3hQ`YQrMQ>>`HsY!PQ?phdLQ&q$b*dHwDFE>d{Q?WPTL9mj;1A&f)sf8y;BH?NBS>!eUiiYMBhSajM-A=)V4L%Q zu-+0G8akNDbuAD0(81Kr8^3Gw34}dN{>$oXaX0^XJ5Jnqcos^S#+yJq?(Zp`s)n^a zzU40<#q?c$q3Kzo9JKlG99nk8V<_BK4R?I*x*tGExwQv&uvKtN zH<9g9s2gTrfwC3bF-MO~9Fwd(joRBnbLyd~jNJ1-1=^{LMbz>z?xUOu_VNvH zZ!cLhBh*)(&iRdCJWP4?)rwlusbHWr?(~0mK=U@^N9DW=hKyVlPhZo^LIJ-@y>t6< z_~|9j#({Y$y7~UEIHuAtl~;Sxa%#bE|1&eGapH|DQK91OChe-Sm`L04+AIxTKn#fJY7&zpe_Y33x)y7&_=nvF8&AHbvzPFJGA5p= z^VI=ZB-S0U!=5qS#^l>C-j;Ke$?6*Bm_@(DkzQAXE-05wv1;kj@(rBI;~}?}V{zef z@GML*G@}K%V84tte(PFaikUQ0Xc8yDc`(J3Oba3SE!R1}z?U3K+UU%O{rO961aov8 zsG4TpJT!4u;=CSlX*E|Ff2kUMc+)BR3?uh45@>c-9(ZF3Qyb~hL7Y$DJe#(@O!vQ+ zM3}!B7?|m3_7)aWhG8NcwXqlxlWS8lhfZ`=+;cT-Z5!#^Twd@-lW!xx;o&QSnL-Dt)a2M6_n ztnT*Az66)~d-1P7-q&vh1MjIrM}fD`Yd{vB8|%5rI6nYkrT&@ZakH&$oeuo0&?bO>2_Vv|`4Ct%8Z2!~GSOg3SKHAou>mBi z78X|C{$6936J!A;U}}k7GK$Z}Wg?LV-^31jpS}e&LGUfEnyoxBGu`*%I$nV-lQ0>U z36ig7Z)^`HZSw{pndKH^z%Hv$$Oon~s_$XBd0mYV^ z)}(evrv$jV7H~GoNUsZ&4FUZ7^*SrNnw`-B*|d$D1=k0y3JrD^IP3QA3H_zgTcscU z=dU@*QZ}ZbemaKnP_@7PY9@8R^{MV1~8UX`-7+%EI3f88U! z;r01k{Pew~t|_{WQY}H( zArJk$q;spkah=oZ|L?t>i22ES*A6PHgPzS*G6q9`dhc`4*@b}Pd$;*~JyiX>mC4xy zeD1Oax~x-}tQlvw+3PNpkStatW9jO#uHjbZF9E>NgF9kFi~F2Nh1`VEho|>U3}wsk zIooV)a3a`PmC7hrf5PiU7vz|qm062GwG8*_^NERK(C+h@TFvm;{KA_!)n7|K$HRb2 zjc04`WXHrG1kRu{3=4X~r0wR`{)g=t?=ESQIpHE$1W_yf!GdzdePY6AJg~}uF&4S# zAop%(^Fz!_vCOwSw+<-kiBLhdNkLlu@n(D|C_>7M_T-^fKU`XU2N7+uoV2Th4I219 z#%2PdxqOtGL!3}%4HC)9wOGHUW!_e&^tT%QF9G2tOs1ReT}BlaGZxgX?np|Z%$ReBviEJZf9>POio zH|b%>(ji&^VD^0Cd_FsjZVj{ay-hw@soE9Kc@t|qGfW?@^z2M1qDkRB8L*C{cv~5x zy1!ms5DYazB6fbHFJnW4l-gGzdw&EBQ%Jak*&y`qVxFW z{r7}E$gshr4&bxDRXhinjMKMo?o#QAg{Z{}Vi{(#C(8c=aqCvhA5K?BNA->`FY{z} z%=VgMz8|Xsqq0b=XFwu*=etCIn4INI1Tp)J<_MT-Nkwd zT~qkSGTpX<3_f9gV=$TRKYwDJ0uAxGnob+d0DN7~6G^`FP3{r~1J;d*>d@>g9iMg2iXARlZXiNprNLqndL?O6uSq&= z=D^E(FcC~s|G_IDME+Mul<-HV8d($Ek@Gx1+UD>;g_`B^DOhv7{&wPHuN}f$t zyjTOZccs`*Vq<@H1q___u!D^`@K-`SR=fC&wUezA$U2If6e=7x<* zIwKZT)a*Je?w}ne@|0o+C;$hi)_C(h5@%gg^vHU`vW&VOqpy4n?QfzjPv`Q=vqKS- z^J9M{!jmmQ7Qfhi{&TTt76xqn6Gn+TD80QYJh()w3;Ff@b+HQqCL*u))?fdVy9SaS>-Cw1C{~k(E#+ON z)J1tEcQu5=^c5#Eqw9cXC?KHL=qVXy8^w>KOKewR7;0*2-%6PyShtXLj$Ak(#1FAJ zO2z5R_P@7)rdRy+l=djx7i1uoYrj^F%80xSu2ZhctYiBor^k8i=^w!l(HKUwxLeF4 zE~PNv(#JlzGszr!;30pmR3h1AK(m4rD;rann`blo1m+-+9e@iZJD7!E+KoDO*gUsx zqSO`(xk5W5mM`)%MdiPPeo#U5x4=N28xW-q5TYN_wL;FAhp*7@{~;LT?Tr%O_p))L z8JNiZ3KA**koHq30B;Us@&|gP6xw_hRhMmSltmKj^#_s?@|Jyy-mrr5$ z=0oGcbzjgfzrS&WlaR3mIf521WrLq8d9l`M6DrqjVT%>d)Hvi?vp?^<2=NR#DV}pCT5+%{CNXZn&@TE(O`9eYB@nSZ& zvH`Z=>aVGLOQ&;Ok7F${@z-BwXR$#6QC~j^x{;mRKao=xkw%|~9Kz(;gq%Eka|Cje z+i5I)ynMdjTvF{AQ*?)(M_yGhw0HK{wPk~KO?$Waj)(8?@^$8*cM5-$mR>!@rsI&W zz+>IU_^b@am;>|B(~Zf6Wx9qHh+pTj4~Sl@-2fJG9rTs47x~E2M(fT!XeYDEETJAXvo_kq zX;N?WrVx2>GL=4@-G(^$d@SDA&YnWTE%bKL%My4-P3nxm%57T8vBz`|7RR*bi&7b1 zzuixm@W#g)YUWbZJ$*ntaT*|x7lw_=SyvWwuHENeah8e6EZP6F zu_oy&?dQke9)G;C7zM0co?;HImmq5DZxp~+rVp?NB^`8Rm-hm9Q4$e9EdkXa$FMhs={uL+j%ob2)!b?23Mz^@tU}cI0N~hnGx3TtwfrXt zKkMmvWHHZ-*V5Na?%Uv=C8Q}XA<^H&>eZyA7R_+I&^CTx{OSK&CW<~tLS)l)Cl`MD z9QJ~cW=9vDPe{nHixks@p2n~eHM?m+vz8rqQGjT9#zJ|7`(n1<3r5iz;erF7z=p}S zFK8+5>7`XE|B7*MP096(NaBO40*tDwuOH^zRO8)D+8q6TNYQ%G$ zLQ9sxJ!sQS{GWM8Krm^6vw>s0+Xu0jcPq(Vd~=NyT{=^by8z*Bk&3{}@`=6*T;bTJrs>Dt zT=7dB+1)1wrW!3)#2}wdRioRAq|Qhok?quOaChJANmxPx^Ms1105_jswXup?nd8#i zwKTI5Ex03Tbuk1tKSpsiP@^csy=F@-b@iNB7aAzL+qDoA&-U0`k~l?_rr2))sMS7y z4*0h794=P=%|~bH-%c03Y2FW5%L$$K>YV0%1G$SvL5F5{xz|@%+Z(k$4MW4drD?BtN}`yEHCH+dotm}TgPB|&%~!6iyZHXPF{2U_m8JbnbZ|&T-}C+!&E-}S zIh2EeVz;S&8P!J3vryERX4=j=@<`hG)^~Eq1fqoQAF_<|e*C?wnmC$!RT*>RTx!d$ z^5b)UC(d_0BAO6=R4@k4aN`1NBe`Q2+N5>&x^dqFPgtG&zZ0|%ExIxC3$Tq?O+c3w zOQ1?U0#zVUJFjAnjh!7ed*)W!7FoW3teFnSxHoAe1Q{KK6!L;(O&ww}`4x^4N@-}9 z(03tZQVV2?=4$ob+A2j4G7MGCN|VZ$lsm$YvEGb2d@V%o{JG7XTFzGEa=pZ~?WyvL zEF;IzVAn7GUqms|C^&9eBhjGM_!O~>rnb4MCc?ET+6uI+GMX1<)KfqbF9qvA=B?f-ltwA!* zFAgI1QdA@r0=mU@0)Ro{RFVQUTPWYwt5A(Oxb`)d28o+J{#n&ggQ2QK!JOV2T#c|E zE!A^MF26?zp;Am1thdY)Acoh$S5(&C-LFm!OOaBI356EZ=z@eEet1*qM~GjJ0P-$T&D%mRwu$P*W9XsHK7-cZhKaigx^~ zAl~ce@ho^u(7An(*hvTyfz|KP8ek--#!3UNobur9f*ph4+r$kCEH-}pd(X-z0P^4y z;5QLq9gY~y{al;nwtVjMC~9qb|6=c=6F4)Zv$Q@ZZ)6+1U%3-2Hih4b%7ZyNy7Nt1F3qo_YPzswTpny3{mM zk1dm})#l~?OvTD7X9CY#eQIiQOZPk2Ewfoso2n;b=Hsi$*3-~fDa{ipZGPXPE55?(~wl-BYr_Wb*0LhM#Gy6I97V3N>uaquKr?5lr}F#f7y(KUq|Lh|JE( zIIz`RS~D1CTtv)-_Wg!71d4V@X5=C}!)#Cm<(P+bzimxKOfXY+bhvuL@BefikJP}| zJl|||f+(cheENA~(Qg&Fr9bB0uX!pv6GZj%!#q4*rRnKg#PrByLV@??+m4S}e!Ui# z1c4z{b9O4fqG}(_ub{`tnX)y9wvq{_yD?j!FJPO72>ga0pnRMX1*XYw6`9Fomv>Hw z8NjT@v~~wPMKuEq+c*l$7Bil?DYbtD##Dq1Ql*PfX4V;V>J_?~&;qFtxiYme!4f^T z7=zk5AFWB5v@I^vYBvqpI!u+7o^Sv(Mx4_=obmmGE2kc_Xck2rrgI&Lm2=xleMsL) zm|&OZQq3eYD4XkOMOlWvDCJb^T;`3jg)2}w7VV1oMVbc4Xz$Iqh5p&&_`6Qg*Y6{} zE~cb1>MH|?g95qA7`Alt*e*h51G;KGQ(%gZ9BRBAt3~bo3Z@S-N-O1>k{G&jYoYeD zK-o)Jr|qz57f3DF=W|a(=2vEU%4)d3TuA|=*xQHy$y0}(&jw^bzXQ_p-@lV#L&K9Y zPR&-$7QFl#>43nN`}vqklifg#NKpW=+Yg@ceC?%K2ILb{USn2OHB(ZP%;cTm?CJ8^ z-#==Fyn50|6 zpxvl1IPIvP=T2Pg`E~t(yx&gE$&7}Ysf?Fa;`IEJ+n9~gYuOhj2)GiLkQ!*zknVhT zIf87#Z-OxK36>UdCT2)!x8D40!0P_r4{)~jZf)_XQEu+i72Bg!&cw@lu{Ehd=kxx=xRlv%Hx6I?PcS8#;ZGNqDZl10ry0; zy;ARY6r0fRP9#2FVP9YpsK`exOXEZdxrsjY-@lvkkq_hwH|bS2eHGLkvp^b@lC}d( znR&LHo)ed1rY$9T1j9N8y-M9G@pK{jM4O*9DLNuegM*|=Xmv@cbVZw402bS@Quv(K z^Y7Fn3CTx4b5i$Au=LXl(})pm@yNV`uBXj|avX5{s^Lv@BM-{!8P|xT9xC*)JSP@L zv+p-Ygnc`NefRiT&Kxju9~iQ5wsK>kJj}&NJZ98Lq_E8&*RseiL|+-F?L1VvX;zhQ zVafkOR+}MDW4w?X2QvQkREh|5(z<8u;8P!)nqbD7?QCauSbpKk*A^+rFyhM1z)|s? zGZz1SyYx)QYOEN8R`)IGR=Rq9Nn?Pm^2S zR8Ftzb;Olzw&L)l&8QZ8@q(jvwvFB(qLev4lk_ylY+`_4w07fZoBlNHG1!-6kkz}b z-H3eUJdw+NlA02D_FDk7@3kW8c=|Sy?6SM*hT^6Oa=Y_U;vaySuBN#2#98V{>F}LR3luSmaS)u_L^@7O&2kME0r@L(ss~fR@Wi+ z!d`H0#V(}g>`W>Z_;+cCclaLG8IZN2T#2BaOJ;)Tz+LKIZDeBiI!_;nP!e=rguK38 zMgs=iq3f%Mjem!HiA{`J#75&}9L~<)*!8CsO8TW<{Aj}~qAF-BF^&GpUzri^Cs|d1%u${9FJ_3wy8A^~^|*>>b8-`L6NHdS#@A>B zd{L~=)cElr!?pphC{TVn=zkgi_aj~t>Y^iK*6oIs&X1KQ_X3?(p1-%pw$;S77zce` zuquIA-X+S2bdc0k$MfGj5!9qbrM?>58p+rFi!#6`!csVDCQvwAkX9Df~ z6&8@jcgfckU73Fs^x8+e^e5@lt@umdZu5Z@Cxc%{Z(g*i+992Fq)z37-gphZJaN0o zgc5$4w?-8wGVU{9@7n6$tbYs-*Im~325d3YUfTv-i-D25&z18LR!(tS!T$?wxbVzQaKg z{5v6@iHCqNb@bdj5ednj$OO9c_hbugiZxclf_S{2KArUZZ=P`Ud2;--=IjYqtr8EW z&6vgC=t%BeKbl%{{kd_n2w=;7QHpY#VrX*_+W$uW+^Q#RcyEy_m0s!>7y}<$_5QlJ z$bj%&|4rl*?+r+u|Fi<%5(jMo>qG-ju2^zMZ}0l{Q6>r{sWmwXEL zl|72Qm9hUy8idd-E-w#Wx%qa8ec5^G-Ix6+n zLU_;j@*txC?SX3e7{3G6SNVjRpF~Hno?P&>Az>SLXj9z4A^*A782heE9%M;DXWWvM z{@aIv6k8K_=CNX+w?8T2>)R}O!tgXk_>cb|7ZDoYCHLZq7_sRRq-;R!lPmfr)T(_z zyLQi9;WsrA+`}%5Z{&rnbw;rh>_chnnU4DJZ%?Q((PSz0NFSdsT4?{_jy}Nn`SYh9 zd4e;;3q%`jvO$svWsG9aoKyI#j}6<+CK~NMU998wQj@Y+{F+&-|BeVL|E+`{hhL3% zoyLj2S{0yPe#g$0;%Kk(2<3fQSzcXvEhFpV!ruxd#wb`JA*U*E_YQ0r--so2nRPQT zjA(RB<%VxhwgK}PC5^9DJPK{toE`AKkg#hksTvIPf` zowjyCGObGbsZdv80Lle%rppt;Ufr^e;=Kpn0k4N|VoBhBk;j;%LGg~}SX`7x1VR60 zXxGT&PLrA+U{>nInkF=%+wy<>M3uS&Cd*&>`}r${_eo7-9CPu4rOoQtV@R{t^SW&?SYVi#czD|WmwR7q(+`$S z)_9#Q5h+KtGlL4{=rohCmwC=J=D~d?l?fV#IRq{D0d1Y`hnT*W>#S^8a-}mLS)hxY z2!(98+#6%UlLIDG)5tQ(?y~8De4L3^TJ2$tArcs>ctjB3QL7s?U&|+P=7UFly_EPFBo%FySSS`A8eD@~gMbepjtNMB5Cx zCF9IMSKsF+GPfus?i`0;U={JZ-Zz^VOUD z{MTlO1Z{PGMm2*8dha#L?IX?d3ylz;W~&0aObTKt=7`D_!<}Q*zOcv3sx)=?3tp(7 zoUNmC`CSQX>V*zZX(>QKz*U1qYH&#rA`HMM$)x@riO29?|6b%^J&1#-iQjk*1>K0+LZpZOHe0#v$tiLe^l-eqfXB2iY)tXF#oV2MfZ*n2Eo%Jy8Ju%% zW$F=o01yBJ$*og%`9yG!teSkESbEe!u3M8(1>{0O)8=K>i6#EC0QX#@X4&1F88_ju zza~8JPi*u=XlK;) z4B`C^=A-nGJ7wD6^m-0#2V3w#n4+PcC-|I~#b}r1^!9=aVK79!Ld@m{dEcF4q z+5n6ds);@7l`Je$On5|oS}Y6FX?OH?uWjCAbG&-t>E}N>I4Zuw;hguezy}nChnGC* zr%`(@L%aS1i=$kM1fxr-g~~^Dxs)% za%jBzZjdV9C>Oxn39hc5qJL%V_owCgoP}u$U+a{qKLq9fBE!+y?w9{kG8UEr9_f@t z=Jg-0khhQK5`_~-;gIhF8LGcTDKBC!y5xiFWXf&zaSy~YAE|i}bA{Zf^K;yUZaXo`2GO60G~&ri@Y|ouDFUU*hYPiH>X!Odiy(t0h~6hC zXz*|%`%~{N$xzS7f5UyW%gHA7a|2$*AsK4Bx`}$X1Frihdbzai4kh>^d9qouR5BGo zFf9We|L&#lS%XH^7>+znsg8oK23#RZw(zN5c_STSKgEG~FvC>VbQ7e*YOQ?q3<(14 z*#2feNH0VU&dHua3}%|I#SqN3qAzhG`3qOD8r{!hV{_Qpa<_;R5^9=pzrV->DLwP%nn+evZ*rhB`dlD7bDZ$Q635bJ%Y+U}#Yg|G9`81f|HQ9nk_n1q!((nqF}O$3ER$7HwF(0{O3H>&PKwt_mve~;ChtFgCbyj0x7x|%6W1AQD6@#< zVqYC!o?5i`$k>YX0zd@+@2gYT$5}ho%pm|8ur&e0glwY{M9;>P-vn22qPM2tq7VP} z_KxAHJ28NUq61R?_TS%m!6sl=ln!Gq7ISP~{3RjN+8|p%k^L2bfIfQEsF&#OYz%Ar zu3w%dFh4Wl&Fl_+h}jE_cfacnzKO`Kk#ub(?_Fi&dp2Fms0pAOq^Lhn-^W*DsAiDV z>970Ak(ZfFm#*$N_cbdo*T`12_jcGy(fhpin}7+%b~Acf?AyNFz1#HC=w2iWMpG?z zV=Ot%Flq*VsU|b_f!quS5^z9Gu}{4uV$&(J6EII>;|j`gSMCp{h7WU%5273H5A1EE z7Jw;&vSDVe5zSnU@RSokEe5wBa#& zO}C9efsRMwVfgj(@y*G@BSkA>OeVdEHITAd+4 zrNd=1{1kaZ`vR+h?*1ycprUI?&ts2e>G3li(rA$;EL+BRZ|c+I4p+?nX`?fdwQYb0 zz_;4HVcd5kt!k!=omdUWBRo2v+L-)uqVv9AkWNZF)gB)){X)L~GIS`NtGsL~_-h78 z-FEB}tB*Z?B4qBa`TOZo(|U+RE9?Wg7p+f#k8<-A>xx45$f1?l@o5E8yksC^Im}4kKSY=nd)~OhL~qLdGboilaKZfswq_rnN)&UGrXGb4 z9Q}GDG`0wtzfRF^o-02RHQ^P_5MRpgG$C=<)8u-luAz^c$}?V!IufA$&A$t@#?6zk z&Ka_HO^tUu7ZP+Dh|NE@DK4!}zM{50Nn$U0*j2ra5~Fet1d3MeoEyg36GodOrFeA8 z3YUM|ZVtR$&2#;JQVANzKM!p48Bd_1OK~{VtrAZ{o?xU*PRt6g{@pV#2tS7>BA+D) zRghz}RY=<1^irIY0TysX{H@-S)@C%QSGR^hSIm&i~E|>NnG+~wlFt*m|hv4 zAwJ{2f-Fl48bi;uWb)1)mP1zh{`DVDfQV9o-0ynZE64heQ{GpiYcd-V1X#+s&W6U5o=UiXDRDmHW&jHq$LoOSlGGtObzDwpCP&0>)o%ZYI7t z`zL9=!h5Cl&^?3oqa%X)9k}xi;T?noi|*r=!`LW_uy*@4t3(C&jFn@Q7&csPL>LFp z9`zk??Xi*gs9#DoB{8?r-c&rICgKJ3*%(k03|r0d4~Ef1WDz@42W@*oG@XQ52m2#e z^c^*c>H{KDWOH(^wsb!PTnzWjyV7oBxC|5voGh%ZbhkR3T5{&v&Pp_?-v1A3B4o}Z zaZk@?F8!X!L9u>qL$L^%pE-KCY=Y8kD^MD5@}i3krc4{4nP!!xmO}|a^4n*+iDWYd zzGXh+DN;&>;ExaC%10U{23Kgl`T@4oGj}S;AODnIQp3A{i0(3v2i^VQF)dgV=Pfv% z@@V0APi%t3H)r?}BbVvJURIn#Y(^2pPT#+l^D640UR}~%s#7Nr0a^Fsg)TWR8 z8>F&YApDSAHSEms$Ha=iBwA?j4NSd*gS8$5f0wN}c~GV;PTaf?Iz@Hp#Ka)>_+H#i z;J4*j{E3E%`;Gt7|Ejy?ws7WgPV&=o$0g5%hJ8Zk&f~UV6Pr#kP?IRDe5qMvwhNu^ zr+KQ2+rau*Ta%eKG+9F|>PvFR`UM#CeRn$M_+X@1N&|j6e#nl4jKh ziXjZBx{_}J2PLt;u@P@{cRQyVQdlhp=1B-d$j_h4-hQu@%NEcft}WD>Imzs;4Kc?- zVwv$7k-;ViII;Y$L1+*8dgph-1i=Wo=lTzHc!Q0#4AXz1d3t1I^;>J%B0&~94s6B5 zanm-TlAwzbq`bJ^OYPfCoP4nXGW4#m7l%VHc3EWZe?Kpf3HzGb_xI^!fz;X_V)*9^ zsbAwDz4#QNEG5-DT!^e#hBZq5v|T`bxhpE(hF3feGJ_9Mohtdjosr@JpIM^PG=}xR zYfZLyc7p(mM$?1IzyhdG@-Zf78C@9%M+K{DG1^fO*rFg%>sGM{x+ciR(NT=q@~}VS zI7yTasiNvT#K(sV{Gqqu*Dr3%w#0&eW0f`@eUDdz-fEk!FA~72p4Pkh1h!93eF3bk zx52yiSol0ThE>)X)un&`q0llFZyxas2|~z`Q~}1HyF+f^f~witO02q3j!QG>J&8V$ z*dK(2(|aU5I6?K2$H$cQksoatgc zwmapJK;cx@MAKJuq(NU?TUisI&m9N@W06J$F7NkOfp>qToA>fqCxA>)SY7^?iAXER zD)}-0TNW{6uMCB=QGtdQ)djwM{CNAx5}-!axVfH5KOY&#LAIW{F(n&48ou&GP+OS{ds~z`u03HgH7qP8h=ZyTc1pUK z7zB-OEmA3#*Kl42=R;CVc_?b3BNqDn*8ul>bd=H|QmCS~g|(en?ci6G0wrcx zVU?z00TaHgp@T{JOSR<*MQjayZTjju4Rf_eTs+Gg3fe2l?t>~=fj77KeGe1IY2}Zm z%f8wVFD0=jyBQq!EBa9Y->~(Tz)-aGOT(E zU-r)eo&0p|OYG4Jmv45x)3%k6M@%k%mPlV38JV;J(dn{LDaO4APANA`K;9C zwk{8s%PzoNkUi*~?084|;n(dg0m63yCH>Nn%`|VCWc=|l zE0|=s&5KSQag0d&aqiNVV}6*e-eXi^gzH5|Gu*h>Vopo{`}yugdqCi~(=~xEN%<5) z0s!|_DGHgpZ20osCKhpTf4j^i*&}_#u)Je)m8MlQd>np9mbjR-yq4DY*ZE(@$A7;o z{?i&atX3?$PSuLGe6y6LbvMy+)=>JK1m;cu{@~A37$?0_hLrnkr&A@4<_-F4kyq&8 zNPppCI=OUjqMsAcNN45)6lvS=m!$3{?m)pc8#xuIKwM!`UPc&Lugp{H73Lpt&@Od% zO;v7N$X%uzY@4jqGYazFgy`*a&^a4mJvi{`Lhi@m*9c=2O}VWOIW-{6f#JvQW>E$u z3O(2~vn7{@AeCV6>Q1apYco2cgd{?-o`JOdsOr5Vw%ooN8YUF3yTvx2m1U7o|zoAkIeX0gG=LEa6mMg-KS+>Xm0M32T%115~=fu(Bl6{I?J%A z+O`WnCP;|VB?!_C64DHyAl)&Pba!`4N_Te+(mfz4jO0jzlz=oycf)u4em~$3a2y+E z*!#NIwbr@jx*Se)ch*5ax^?vD_XI^0{oq0j-V}bMD#6EceX4J3=Rle-n#UAW)BzFMhLf2stSduI|0Xt^cFHCUb^E&V61qZ+=!*V7WY7`Ms(oXH%SYB z#<1~7<;@%^w~^jc?Lr$RrnVN;v3G{&ukbR*76nFBJoMKWBot*2RoEZF*j=f|%2+e( zUYp??pA$IZ?TY)88t>ll2RS$8?S322q+?K;=`L9OdqJCA#!x2A27}bVe}gxd35%-6 zI);tpV9wk=BkQT}Y)shMU-3igUq}u(3Rou3 z4p1TBY5Sw@@B&ej`^k>GDQ)d}eU0Z1Y6kp5Io?N4D(8M23pC05+X&>I=pZ0WrpHf! z4gw*P;bs;jv6qbil|uKiv4wP7rQKEt)ydx<$1l6U5-YkC{~z>J)&UF|n&0)= zzQ0g3B|%vHNBT6j$a9Lgm+Xftrye1R6k!~k9aQNSK6AiTN}gR|DEf~n98%PW7Y;4> z$&nsKngjI`Pc~`F4#jPLSQ|zyivkZ}Aip|5fN4uWKwxM0`OM7x+FhHGkndFxeGKLC zO!4r=QN-}YNu9#QYJ!>f-DUC3Nw40~Wu3z1Xo~RiO0mMl=8^=ch)M-MzWwHtSshn2 z0j=3AHGGQ?b8t)Qmqjdvb-^N)@OU)BB!65~O%Xz!|Y`xogut;!QJlyWMCSc7G z-pHuxSYJMgOd0Ml;!ATpTM~5RX@r_8MIzo?&@iEmUKi<^y#qD-rN6f#-r)`ski> zT4b>#GGEQ=b6atPhFJV-(49mgS zP%iS`U&Zg|*8lEkW2UeP(hdFhpYRi@j~`Uq{6}!UPNfGKd}UWswj|2m1~DF?u2|m6 zh?VA9r>(z#W44&EHI zr=F3zFzF+-H-D6tGAJb`Hf?rfid=|dhCgno6YAcl|E^UDiv z#Q1lF4}QR!B`AV<0^Io~&H`X+=v$72qWZ@(IZ)r4dCmAK3I>#&d{(}=<86&n_%4f-TYh z%Cx7Rc?RKjcY4A({ZIJ2-xZ2ov$fs{yl__Me_Y_9Ne!x`18ZoNXm8wm1AxaxTf&Wr&6YJczW#qu$$`^~5k zV73m>=XehQnlbqB?dny?IXXGSqY!fcez)+prR7lw@b^xBYL;O@B_ycz`hD032cw`??3~r)0KOb^uWEz3gyX?^ZmtZ2%+> zLZ(H|$7$fo{F_N;Bn2&TLpX`FXaWLF2(bc+qX8l=bO7Po5INk5dj+&Ssc zL5I}yXq&odZdter7V^DxQ+xAkY3{<3y|bxpDz9s=oxQzx#LwBV0yzhp+S{qZjG}R3 z4#-n@qid`|>KJ^ob*p|-Wu@^!_XlVsOD?080$j}Ui;Rvtf}SPe3rbMERIi!GfFa9x z${;JL3R5Zr6y+T@;^&7xx7BwP@+?VRj#!Ynv0{tQzDoG~V&-Ywwed7c1Dos7!Z){e znzCk1@F=5He1}L2hH&u9>7z&C$&6!-vpN6w=!x>8DyH+*cs&dGV)24G8}V5Qm*q!? zaJI*Ai#Net4LYTqD?7VrrUlPFlPMBB`C2hGQZp?ujXZfZmjAt0b_`Nb>Kr5ImqjNP zU!BUDCTpsZ{7CtAa9-4Fkw3gw`(?oolJo3Jw@)ow@cR0WoMKFAV%j=xyA1= zuv2#3K0HoAR!vO8J(f9M1BkOqi2&zl0Zn3?`2lnl=L3c@N0TXR^e$3R7B)+WCUMOhJ!r~x?`wr+A_fzI z{mtz;&q=6-Fb?1b(+NEL=EKW$acbv_Dr=;y&f=Gz-q{mgCGtCqiG#;jOtKaAy4QoT z>>Mm<*~o@9SdyKhfQ~OkZiSmPriPDVJM4<38O;sM z%F=1Sx>DfGzy|aIkLz_o6D0#u?hIb!5i~#?kNyPkI@c`w=iKZpIJe2kDGa`GM{nQ; zas4W55QpwO&{>~l)L(CUF~X|(gWUB@Oe11AUZpKwf2SH}Ix-q*4JbdtqF+fWO@8A8E= zAN0le4mxunaX`D$bBXi)C3lLp&RFrXFcSe{co-9Hpl%MO&n2z!{pIj9$#aVa{?)lt ze=1pTy>mcP;{#PWmfM-w-jrTsLQ(l&Xyp7}XUs1wu#vpb<%b>??^+3n6^x>Uz!j>4D&Piet3zkT2Ywr7{MzRTh7RbGqbsAh=@>>$& z#*@-g4eJC)C336nw$c;1W}t3nKnD+WQf}u33rl3SF>Y&}WmCGX`zO1(V4oMTB?INF zqIOePO~MnyYX@@6?Oy?$m4^IAW{&Q1D*sTXDQyI-aFA{6JEji?ev5?H4&|!^=HOkv zSqdf0LlGh*;wzWs#2gF!m_#I9qg`RzawUXo=XK#cWpzhKjK!R)$cqUpV^#e$8nMsS zkfYKu;ZIMt2ScF4aUt^3DJD|ctGvlF#J>)#Q$B#|Y%Q?Tt2Br!Pgd2G=hS1Pj0sb> zl3!?h6M^@!2#Hl`qq3T|_ZcIY@sB?5`!)9CQS3$ETLE3+dpuPCD-jd-b*GC*NozHUv@;hS&XzI<4W;U}@rd8HTNvdMSU%O7S@XQAE**#m8+-{rO_9ba zg9gp*EX`yi^aQn*sO_n~h=4V^uCgciFkr8@yIf6BP?G*KCyJ3?f{+U;PQLNhOw zWjzu0dB5d%iyuyJ&4D&vowk$P>2p}Q$JMA{^!XJr>_Zye$1*OdOeO1x{f69-|A0^c zFmF(lqmxn|-7X7CRzCRRvZ=Qf^ z&6tY_zY9#?s};&aGvQlC;oH#^@4NnD?~^}NdcJpzIf7n2hf5D|_7{!p8WpO>d+Mxb z2MJv|LYSl>jEZ6(vb!VqY}1A%fK!>Rxorg*il^J|j%=CQ_c*=5204MX%f{i$zc_lw zr)JE9#W$5QCA&xWvzY%6XLck>6g^~$IT^)O zHMrjwB|4^xBwZeXeR`)jn`vZXE3dbZcOm6yf4a<+r4G>C`&&wNI!9PYdSxms@U_FHrF%ob3;QRIw#=h4X>Wb;BR>%9TED@4iPq5 z?g$x8wU3$sudYtcT`f-dS239K@Zg3r@ehJ=V~v4tAw-URORgLkX>V)af8Evnsc&Z& z>M3gF!~|1Zr)@7Nd!QNrF6(DpSK+)RAjt0OlJz!}l!_GdtA>ee_l84O&aOhhQbe}I zK^r~n1syuD*I-Fi=&mdTUZc;^NzfZ$!s1Pz(KbiW%SCiMcCVdsIjH&9D=NRXAws#9 zO#R*2-QTiZ@eJk!wV$}sNJcEt;!IRLjl6~}rE>azg^FMo8hlk}K-`3=EL*jZt=Eq0 zdFeLQ_;U}O&A1Tw(;`N_AgeUCR%-{RcN=G>1ED;WUopg9Zk&9275>ppuK=KZhD&(^Bx)efCj{J3* zo?o1lHC3sNo{~iAH;tQ(?APd4y%4F@F8@Xr^}V`YXsB^|W`4A>Vs}b7XIQEZYwFzP z6xG0WYVwU{SS6(G4neOIFJ@$0wNRHi4BXl!T2@H zJd$>Q15zwEw23ZKaGsXKkAstdC=d+xBjd3sF{dSV2#{)a_C~gx%^OKiu6%$2gZ$Ob z@#X>$j(vajDhRLluYhII!JEBBuOH*Z!|q^j53yfN%|h^albvUsz7PYPpfzgSke z6S!aU``)V{w7j_)>)RnwHDxlg|MIDjSayvjgaMQ#xoSXCGIO$kXgd^o4#Mp1%eO5& ztzPFLIQ#PMr#D=SKG&}ny-(@ERB0tI)Vi^=Be(&`8rs?ie@~ggv@_gt4$RLwj*gB! zW$JcKtul6A6g_?UGT2gY!z9-nGOxR#+n zSMu^r_@#GI7(X#T%o>>VSnjs(kMt5~*bytZK|;aGKQyJ&O~5P2D=jsaX`G-41D#&+ zK!C~A(`L&W{K`Y-?4@%RNY(ZsC-C+W=yCmiILhAkAtq}Hbep8;Rt*mg#m>(Ie|N~l zHnuDyuu`8i>+tV#SEvQ5m5B*rJ@d)&R75ra!ke8G_siF*a&Q`{0ndgm7ijZ5#i=LR zB2Ji^6+Gdk!JlO>x|Nnsy9PjkAvicyU)1+L8NM^?P=!fl7hNC*M>`*!$3&NS2x*dU zu|GAFHGEjfcs4G7AQdtY)Qjw>bjfFQ(-~6O*+0wyq!>$srlf0(TI%s; zHiH*-)XS$K#ua{_@#D+OLD2fSV6%W|fcPtebYespKJJuLeJkJ`rCaBNg@YKt?k7Bf z%u%sAH)4Kw(2kCN{_>OI-8n#0X%v*|f75Vqa*1LhFkmShH6JnOVP^^Coq7F3ExW=Q z7ct-{)auh~+1|y$5`;@EV(V5+Vp4?jWL`JL3*cUe(od~YwNPsKd?6;5ek+7y-U8{$ z569{irSSsD=em!5JhKQjlkMHy0!{q(?dusMGD{iy#;|%_!BAw3SEKpX-kNOKfj#(h z`Qs4(L1{8|>p8k;;8E2}=zRu{bw@H!pL1j-CN3dSr1-&e3RK=x+gjQ$L0M@0JEO zWSs0@R=#AEeY!idyL&oU|IN)gM!u}u9J;)E5_wKf?|r^3aCdMmd@wnscl=K{-gR3v z6nGA}!~oede1G0?!Rmgu{G@8Jb-g8Jm`^}pX{}7X_1eA%HC5$te-{MOCxDG#Qc6Bo z6LuWb%ES&K>&0ZV!9hA}l)q8S2;xMA;l9eQv(7Lr2;2UKZ*nQR{$C!KfAX&WGbxOnIl^^ zHIn|Hy`6qv_>bq?f|eGR_uYArlTKj;(9HF$0mvrkVcar33C~b7@lZ8>TCbCh`#`;? z?WkNHeb9*PTNYtN3a}BR62hvoG@m@5T@}(70c|I;RD`MHC3)XqZ(!m)-yDsySfftWYW5pX$IkyFVeM6%!cbEKKk~z`` zJGm|D=bWzu(+8inZz$Q>Vn6=yB{5(Q#Vp=vbi_F&%?zljxE7z9d76ZOR0Y#`MB_>y z3%x=MYS)^)4Alm4q*h}2DZEo4y z{F`iuyN?fQ@&1gN+hM3U=Qb5BCc8$v2tcf|8Qv-l{BVG(Dp4i9p0##XP&e=I3rgux4jqC=Nq-0F695By`KT zhi)u#thA5)vGd%k`E9&-Ua8b!Q%U5=HT`&ApQF5x_xBoE=v?07o?_w4{NE$i)8rH< z-`seAxMAfF2AH>Xx&UXtD<8DEw@xAZ@USlCvR}b>0F-}q#`v8WWkWqc5!l@qG#tu< z86v%dm7JTzoSRj%o0DSUjd2B`jU{9#7MdvNxU%3Rdu;HvOhkJ75s%Uj%Wo51VTXx_ z^DI2-Q<>SBM^}?p;6&9wGeh$4G^GIO3vP>TtACHe)max6K^b{Ads^hh?Y>{{zd|37 z>*x%{+4nfV6Eq{*02!eE|F$Fqazt*wm;-RG{m<6`4R;(q>~``D~-QM}f!rpTUG&E-cn+n`| zxmpOFl;IK7)DOH8qqp9(DT@?2Vkvyi|6wV-j+%cCQ*NyuObr18tb3&pT zX79#Q;SZGAh)*nB1VBIiN1ip7x#=-o&eDpnOqF)~AdMo4>|wJBGSJtWZQI~~KIr*{ zNo8nQ9{rKJ0yY-Q90FTsXn0vQMn58;?AU3&;Mt*Nl;X(ONp$PbQ^$VOy0Nz-pn+bl zFP`|5v|x!MLCA;zABqjA-5T)M!_<(u*K%);sNb;Sjafh9=ta<$GJQ!PNF)gehm1=% zaYp<7xo&PR-V!yDD&ah6gD^u-@N%X?&KgLHGNW1b!$h?mzYOf2|y6~f>?llF4l> z*N~?6Py^{8|Ha$0{b$}Wp*R$ZNdzhC>I}5NM>I6V%21aHjh5nhnWy(=7-I}V3#cef z?Zyq-7VHfhd8Ogo5i-fZTS7NBb_mU5`gL;=5px+KeE+PSxn$sr){BFSf?i;B!2{iz zP82>Tso;(@LUsbm2l?mpXC)cK#sD2_(6DBC{VX{mLT2dgTRs87mF4Ats=Et`g?1kl z3W&(}d#^S&%p%PI8IZ5IxH!|BcV!KwesdV3ci6jl|Cwl550q8)%+G(>a_$KoSAna% z%D)tPaPK2sKW_;ref=_7*+Whh$g7210|D;rjcSk`0(L?mjooZz2!EdFWV0X!&*JyB zT2(Fwqa!IHT6N-eqqz*2OeG&^Ep*w^AsTj~R*U<~w~7hKr*nVw3T6t&cgMEREf88* zA)?XI`-d0(cof1u0|{WD>o3kZy4=V)-bqjZhDCZX3~qRzb#HhdcBgpX?2v>?^n#xO ziPp{2tNK~e@TDsubd8@xfHcmDTT9-LJa}DB+ib zRepo}e#hrGOE?{CZhrS@@GGo>L#lr+rsB1Is@F)t7(I{S1Zll&g67 zn%yM*iibN#DG+AyrY~V}iiC`{%BvP#-7Ppbdpv#$&GNBHU3HIYA>&N7B8%T4!|US; z!bN`7$kH!LA<-3chzfScqU2v7b?JAT-fbDR8^UYrU;hd+H)WKh7Lns~`3!X#C3G}%eY?SSBOMNd6ppi@~0hH%by`S-~w8qvjK2uW@!`MRQ%`$1RCDc96NQ(&`%NAOvc zvWK>BR15DMn+adWoBq-7$1O##`)O|-2+`YqElcbBeoboJv_Z?kMF4cZ?JmYM2VTbr znLt0OFr=vYQO?=cY7|fF>JV)|DB+YVTO>0~CzMLY{gpBCNlsZmsfW{3(DnKG)*0ty zzbn_Y=Qr(tC&+9by9JBkd?Jo4`5N+!VoKrli%xd9g)$D*bo?11T~$uUdxrkpxfQ*8 z_4}tr%r7o3bM7;j?w(r^;`P3~zXtqXC~teO-@)S5`4*=@tM6$xge6Tr#rw8*qbXX{ zJe4@4bHyx&ZF;fW`4tE3=n6#Hw+V14g;wtlUygcq&!tqkU+(fald=f;Tm{La@&@)V z`crznFRf#oUBb+03bz?e(&J2?WVncA(j|h~ge;!M^qY3Pc$n!0Dee;>9>d20(OID7 z)UZOJdyYsmep4rLYcuGZ0a;{^immM)Af9r{e?^Qv8jt_a^VT4>sWL+3{7^A|V>l`1*_Rj*EznKYDj4zy7T)#`~H3UE%R{V)f#FE8p*) zZR^WB0i_~NmB#_{E%F6Q^yn;q2l++*W_hJ20+p<$t`6A4d7~eJo#BOVQ<1wSX##!2 zFqho;?tb8V#`!XZ_@G)y|8gW4(Rr>GSrUJB*l?49D$>_dnOhdz|FG}{g0)C+n8nHY$uHgx#3{+ZKGJ$&*#==$_HtI}+RluCXbz{g`)P87b~a|2bV z5(uf6{x(gCCeOxRqky8$HcfUkW0pfD7|}GqM&727lw896{L_#XesQbsbuuptyH4A= zeH*FcT<`C#^UIT&H_NAy|GO*gz1m&*#$DE(*^)t~bNhWna6rW3;i$Q6*(CMMNASQR zM|Iai!t-Ab;(4}FAls-AHbw$;Pn+K^^FK#efb~0Iu&nKtP0?6<{3F1bK_x}H)I7w! zh85xuQJ8_KgXuYF2@q6Y9WGYC zF?3^c&J8rSCu@~OY)yP?&Qh`3A0r{B#-bd5{*3e$@Qr>pQm&ff6$0wYDt_-M*Q;Ti ze$_DbvbuhiAQP4SdXQ|u;hBc_+C-Eq13C8)@WJI3hW^bifAW|LgE}rG?*E%Z#Fge# z{F3~|Ub%ZPsJ>`_e?b94bJ5@yrr$yc@T==v2eias9!|hcRV_nZnaa>@c6KBHwM%mBF{R+*U@ZjX4bbN5!1K)<k{sK zX)9nH2H+mG`D?Zf3;{In{a?TT@5uyo{*&8)$I)_2P|ZV(m+vPkekRZlgZ>jFrSG}h z&(K%8&8>sW+Wr=D)u)M-IdzDgqbn4%_xYgu&E5T77G%?-nF{NN6rjCDT1*l*RTd7>Cp;U(DpVrlN;U$_KY4kD{$pv=8ZjxABnB*q z%t2h#1F7u(jbd8uUpD7Jr+=BS6y#bp7&WC!6;AiV0kWMVxN_g@s8_Eg1pcV2D+)rU z9BBs;3Vw_@yb1?7oY;u9W!|^P#}SYrR&>ni28+f=kWYAz-@=RjL3`zuW{o-1*`jc< zEWi0CBtluG#uGV{_B}Y#VrkN}bHqNpdsI8&BA2~ktob2%m7%DxxROgO8$5YRjttKZ zls+m2t$6($99;5zP%w&a!}m|uySYi}SgjMj$p*2f^NOjN1&x%wc9XGZpB~%#=lyVA zYv%TVu@Ps!3j=*xU0!w_f!odQ*`XUA8p%)mx#qqCGynj*84xQ^anaM$NA?;uI|;5g z3yLH2ta({5MY&e%h`8n)&2V3K3JNU>{8?!8Vo*88@jWsEfgFo$Cudjrk-z#(5nJxE z_dAJqmqG=j&oqUSYT`EDHA`OF-QN;z3g4yNpP5O;kEj-%+{z}vUv}E3ttEQMb7!b? znuwu}s}t!+6+F(2LlbUAmn~$g#$QIb!<2sdzF*YigwaN-pnl`Ui7qJ|vJCXHdP>-5tJ>r)zwkd-EP-|xSFLo2u6!}RCi+Am;o_;P$g zyfhIik#jg+!jX4c77o#kOu+eDe#q{P7@d+LHR&GPLF~{1CxButQ zMcu8onnRfK##M6$CJulZbb&PMpqIgukB;FL7R)P=Zmx*sNT4tgK-Wyqpr=qEiFunE zDdEVXK?u{-w+yxzsBwPb{D=hxf@Q5LU<^kLM;-XIN7;{l|%xa6#tqc;5a^ zoRFQ}S8=9Xhs9s$H&PVYKwD zYs5_GqT0ivRxBlYMPgkxvzeLM4({HvcJR7~2i9oAF4gtqdaxpQS-Cm*0PX2@_eLW- zRw|Z+MsipY!FY|e^^k%*<58r13v&5}+C*td^X>RyV!wV+G0T8#Av!ZNGpDrYm_zzV z$0j(GIJG^`UQu5CUngUDWUwvHFuRiJps{05_vHhWjLw4ZF71$f%*C#qAIhDBN(yrf zT3l}R;9(+dJ0-ek1IxQ_33W~rkT3ZS6E9?oN=h+$5ulj+wVVokG_l|PVaJ(KRUoeU7R^{?o_tXi;Ls6UW6YBRe~Vho3$Vm+@5A`FKR6=y`@Qt0QI> z7S^v$w_DGLJM|foki3XkX=-T^d z&>Ws#TwFUjNv*2`l^)R~QD7euRNmKjln6mdsqlS}#I2bsw4mk)msYMfNi%wFMP1j< zE{C+|q>hFgdwlrg)#`l9ujl6zgDen*POVq)Ec}^GLP37IXr|{Fxj2Lc2YbA=v2{Eh zFi}%ipVrVbyFTKUK}D38m%nhQu(BcpV7a5#*Tb0UPfMGrP?#Y^Z{CW(x!QR2;F?*k zc-+LpSqHy&16J%1`g<^F1FvJF5Y4 z8%K-3q!svC@*wy+#z{T=F5hCnJ+u1UoAP$W@2tw#G>z1zud=q}$7(LtqdAT+I=rvU zkfODHOWxme8;1qGk!VC8@#27%ZZE@*l}J@S^)wKYvXE`^>0^H)WLRVI_n#!Psu!-J zqy?t6joIO}e*n$6NGShtrae1p9MO{x%s((iwkW?* zR5FYGo@{`rsZZ6&VWJ4}B;si~iKmmrRFM5kjJ*={^?|X?4~E^?6F*y6E}M7Rdw;;~ zjtqSh&#*o-Ui`bmDdld9Z;OU1UG~Qig;=4-+qg?;!KCfWAeQjb7lD1}x#%{7&@Y50 zx{kiSL<Oav>|@oY?Km)XU&sXl$9fO+ErDbFqFWcxe9%8*ZS_C{{HIXDo91t3_Ba z8q1rGD+fv_CAbU=Mw7pvVbj`qwArSpP3JRUKt8=A)v0c5L5&y3@KI)>BCB5#`>%vG zvJWc%6>t8!tUSpnCx_dQtSxyQq@T{sS$o`I9lFp3O*UF-dJfYu5~yWoVCFMr#Ylra z{(y<9wP3T?d64*Se2PT!Pbmb$d$?dKOOz3C$61@bMvWskTJm+@Jub*gq?UUVo=?px zbG1PP^=lvQ?j|mJ1xkq4Mld!QnedQU<}MpE?VedFdfz%ZrPA~?4Q-sV)T$y@(7H@m zWWsByMTgzLKNU$k(#8?Z$Ry9HIll3)wf&M1)XxccFmws*l_q%Z|C$9j#iVd0 z@HOSLiD(=mmD7>8ZwS2vuZqpIGjVjS17<(Gk25hRr4aTI(Zmm`h+PXV($vy*a$sLQ z*?deE`2ASwII@CyE;U+Ii!f2H+Z{?RkLx{5F!35&;u>wjLp~wJ+NL5n$w|_AbgWT>7h$c}=&m1szFv>C#TCLUR9XL4$K=!p9LOv1}*yrr27q zbj9JLCj+$yJ~6McwuV@1YY_uuW4Hj6veZ+x|^|okRW{ZjMDey zkG=lBWMpW!dxx!cixCp8ALuAN zeh3fCBc%}Z?3)*GbHCdmX%xPX%<5e{?A{&rI5~KLv*(B01zFj>tSVmUG<|e2s2Oa( zIg;6%Z$Z^<4D$&QK5)gCV?+f&vK3d;GJkigME1OYpqU@olN zV)sD(N7t8^9tQ@kBfvz?N={Glm z2N#2!SzhnW*hAf^l)lr*zbY_gOa`kh(K89rr$j`m%k}WDVhMo{W^2JFdayzc+^`P? zJu)x?-tWA~jwz8{W-xFwOQDGmQa2(7AUf?uTJ%SSdwlO6|MZ_OxbsJDw5YzzQdgNW zmJ-i4QSKU4CN=J}V-UMH{H{MjA6-BsQHYGd1Y6i!@V$YeX`^bm9gt%YUmd+Wh%uD*bJFDsv%z;0P?-?y9Lmm^lGGndfti>tvtHeqw zQ7hV7U)Jf&i8E-`IGs-G6OE?)yFl$!_!DV^*LJlIklybI8yN{}eu4s-@jn_I=MYr5 z>vUHQOY{5-F&0%gHElFK?k`5>e1`RllgROLr}+9@3&`J|kBl+6n5;X8M`d=^wKZ}D za#~pwWf>We^0&)w0>rb09I-T2RFjk@T4N3k5ObPI4_~1_JFUZ<;f!S=r4M4|a+IZ; zYLPF&v;uZ-e@RW=C#76upLfl^yr7CUVV~Nv_w@d`H41oiMibap%reT4m@wMCPGaFD zTS2dW{rY9Hk}o^PYrspKJD_Q+&CKs4ID4#$QP5PR?X;S@dZe$H^o@c{9Dm%Ii(0ho|p{#Mh^T-Ew>ZohS~rEy^c86&hBNa@{Ifg`k95aZV;A$&na>Fplz zX@7CK&CSknWs*$0%`;eL?o+*&j%UQN;XEUg_{vMS{ol0V#q~%+E9Qtu2_mS+RVuv# z#qs7+qqoz&klWtxrM;{c5T%IScs4JeQOj^Szj(*u)_xT}YX04wGvCjZsj#fnm{gAV zIq19D+R?J@JB~a)b~@4)=bTVfqxHxR`;$e=P!b*wmFceyvLI(C#r!Rjk=|UH8)t06 zj+si(ea_}yqP3Nl{MKG@c9*-Y(wVeD8N$NiXmX7Ks{ZP4{MCr75eRuZ-_Hr-U^nFQ#$G_7@+XB=H zGs4;HxPDH@)9JV#3D6sXRAfCg1lhYwhm6CnIK)`2sdO)IBdCVY&-kE12KMave(cL9 zI{|m^+kH3oro68&o*`D)qN&@j{xaXVMkGzbs8V4B2n)kc7`@!~t5wdYL?0GKO`PRv*$yq_r(gq_PMy_bA7Ng^cdE-V*it{B8aIZmp>yt;Djnmd3i# z;pRWoUG|qpAy0SBlh-$9TI;3lD&>@nPp-`(dwk$qUk=?RDd<(dF?glJJ6!{20E_DH zxrK`tq_VvBn@g~3qXhN@CqpoeJSx3z9B*&F9eH6GX_9J={Fp(gN!5^4VKry1-5#`f zDkfk4;SEu|Nk(E+=C1JkOd9Ni_CP+Ip)?J*w3D4h>vL~|mHMVj3+K1K>?GxPhoF7OnDGq@4L*Zj+=)^xG2T0UlqCB zxuxBHGim$P9+$Yj(}vJj2?UIYjpfv#2ok!G59y}B9UfoEm%Z3y6KpINw)j_nhMSq$ zO1s51I5?OamUhH*zcID5yEk$s2%Um69+8a6i|Q04F*N17cP%YZd1RwnSFaB}V_-XrBK0!ePPV(Fa^0Z`=q1gdk6iS9Q0E%Ed#Wf)=bofz#7DH35 zIK?1ZN-3tKG{rSA0U3(VsoXD+KI7b zaPV$@33kfRZzh@T6bFHbrlp5)qUBJh^70BJUcyEhLrwG8Vb?p>8am9fDErNL|=6GC4fkJR~=Y)9Nf3k>ny_v-tW$59NrZ%0Y z^fa59JLYioSxmdf_t8!&17Z;shvW4*?e=E5OxcSn;d{({2E*eqTHlib%7_m|9J~U2 z8)u7t@|#~U2A_E+H*&il-@0{jm&kSBEj{oGMT49)w|bwFCO=r5ZyU~<%fY*PaKBl6 z9}A&0+De7YtW-?>^0-~&ipI$~0GTiXpd)OZR6{W5xJ~Nwu^&#o!J56$#q2#c$BMF< zGnZKpokbkHBAUa0G;l7l@1m2C#OI-@2;#>u_DtFSh;T?0P+TCBnj@K`+T;x1)3wcZB}g?r#NgMLCQ z=lJZ-B7!>QMGUarWN7se&a&Cx5*oBb>|dv9nP#+udDTZNJH}|n97(fNuc@s~!-^Tt zTcq^VV47+W9t7wfEM1FT>LD>;T7SP^quC;e1FV7l(r3ZvJ6Y|QX zB{Cp?q}sUGK@!Y*t&NX-cuOo+FI8d6dCaw$3}I4c?=C ze`t4yaxhA#lm0~#M8@KO1ZR-yQQ7l8q?J%`OT!8wCV7mAs4&}7|0ER4Ah+e*;{E|` zE+to5SA81MKhT3QRjFfQ;}ZsF(>StAXHKP6Vb|YJT7=jCKBIU-4;`MIty%jkBdW;d(V~!7B!!2gCB|bPRW36*J z<(P#P!agJLOXEM()Y57abhG17K=s)&e?A=y=aj2S{3&iNI=U62-QwsXp83F}4^$pJ ziu$0Vx+A!_alAMmB7MT?QZ(J#+TV5ba7jBNzrX*Oxc1@S0FM6YIgg)>Zu@x|qx$N#Uj)QAyquSB^4?V<(N8_Bhe7ji0$c z_8F+kEYjyeG!`Puu;Js3r;+GJmQq+bIp?6mk`w*(qZL|3!}sG}BYW%QXKY9PH`kEWUtOe*mof6^+oW?szSy9UMbrucQ&`?+pUMFpIv13=R zTs(Deebx-wU()1lgPFH*i+D2SoxOF@goQ8n7bL0 zbKiTkEBfM~cK26xyoMGQpA~$Mo~>-f=G-s)jZmIxE>8bZxLZ|trnB`>O|#pq<2q96 zKA^PJzU#j#yra<=zo4xad~xJxKf6M+I51B@vb2;}JatGjc(TZ~@LOZxkZVpW&O zv6n>jIF|>vR;`z_Pe1E8dAolRSyw>V++0x{aR+_-JNXMY$Lf>>Hl#G9j>wK330`u%uer;CnObg0c-WGHUx$)^V}+Gb&p6ii&-8gjb3;_*Wk z=lDt|(*Qw=>}HGd9v}5z?^ylK`{q@x1Y<`LQ)28V1l^H8V6@oMX}N26!;X9}omwmZ_e)4{`4k`h6^Bo8 Date: Thu, 11 Jun 2026 10:58:24 -0400 Subject: [PATCH 02/50] fixing errors --- .github/ISSUE_TEMPLATE/new-blog-author.yaml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/new-blog-author.yaml b/.github/ISSUE_TEMPLATE/new-blog-author.yaml index 93e7d3a..16a1bac 100644 --- a/.github/ISSUE_TEMPLATE/new-blog-author.yaml +++ b/.github/ISSUE_TEMPLATE/new-blog-author.yaml @@ -1,9 +1,7 @@ name: New Blog Author description: Add a new author to the blog authors list -title: "[New Author]: " -labels: - - blog - - new-author +title: "[New Author]: {INSERT NAME HERE}" +labels: ["blog", "new-author"] body: - type: input id: name From 4c26ff56efdb3b5d994dbcae7b8dc26e2a518b29 Mon Sep 17 00:00:00 2001 From: shoverbj Date: Thu, 11 Jun 2026 11:07:53 -0400 Subject: [PATCH 03/50] fix typo --- .github/workflows/process-new-authors.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/process-new-authors.yml b/.github/workflows/process-new-authors.yml index ea3d3f1..20f5bf5 100644 --- a/.github/workflows/process-new-authors.yml +++ b/.github/workflows/process-new-authors.yml @@ -43,7 +43,7 @@ jobs: # Define file paths AUTHORS_FILE = 'blog/authors.yml' - TEMPLATE_FILE = '.github/ISSUE_TEMPLATE/new-blog-post.yml' + TEMPLATE_FILE = '.github/ISSUE_TEMPLATE/new-blog-author.yml' # Read parsed issue data issue_json = os.environ.get("ISSUE_DATA", "{}") From 6b154dd2e968d67c114a48ca355d2c464d2a7cfb Mon Sep 17 00:00:00 2001 From: shoverbj Date: Thu, 11 Jun 2026 11:13:43 -0400 Subject: [PATCH 04/50] change all yaml to yml --- .../{new-blog-author.yaml => new-blog-author.yml} | 0 .github/workflows/process-new-authors.yml | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename .github/ISSUE_TEMPLATE/{new-blog-author.yaml => new-blog-author.yml} (100%) diff --git a/.github/ISSUE_TEMPLATE/new-blog-author.yaml b/.github/ISSUE_TEMPLATE/new-blog-author.yml similarity index 100% rename from .github/ISSUE_TEMPLATE/new-blog-author.yaml rename to .github/ISSUE_TEMPLATE/new-blog-author.yml diff --git a/.github/workflows/process-new-authors.yml b/.github/workflows/process-new-authors.yml index 20f5bf5..85dea89 100644 --- a/.github/workflows/process-new-authors.yml +++ b/.github/workflows/process-new-authors.yml @@ -26,7 +26,7 @@ jobs: id: parse uses: stefanbuck/github-issue-parser@v3 with: - template-path: .github/ISSUE_TEMPLATE/new-blog-author.yaml + template-path: .github/ISSUE_TEMPLATE/new-blog-author.yml # 3. Process the data and append it to blog/authors.yml - name: Process Author and Update Files From 625b68ad0062a05737262550be6522cae0526166 Mon Sep 17 00:00:00 2001 From: shoverbj Date: Thu, 11 Jun 2026 12:12:25 -0400 Subject: [PATCH 05/50] new blog post automation --- .../ISSUE_TEMPLATE/{new-blog-post.yaml => new-blog-post.yml} | 2 ++ 1 file changed, 2 insertions(+) rename .github/ISSUE_TEMPLATE/{new-blog-post.yaml => new-blog-post.yml} (95%) diff --git a/.github/ISSUE_TEMPLATE/new-blog-post.yaml b/.github/ISSUE_TEMPLATE/new-blog-post.yml similarity index 95% rename from .github/ISSUE_TEMPLATE/new-blog-post.yaml rename to .github/ISSUE_TEMPLATE/new-blog-post.yml index 63967ff..9c63f6f 100644 --- a/.github/ISSUE_TEMPLATE/new-blog-post.yaml +++ b/.github/ISSUE_TEMPLATE/new-blog-post.yml @@ -24,5 +24,7 @@ body: # AUTHOR_START - Benjamin S # AUTHOR_END + - type: checkboxes + id: \ No newline at end of file From a5294b0ce5aa4c8436567687b13a6a0532fd969f Mon Sep 17 00:00:00 2001 From: shoverbj Date: Thu, 11 Jun 2026 13:21:55 -0400 Subject: [PATCH 06/50] add new-blog-post template --- .github/ISSUE_TEMPLATE/new-blog-author.yml | 2 +- .github/ISSUE_TEMPLATE/new-blog-post.yml | 54 ++++++++++++++++------ 2 files changed, 41 insertions(+), 15 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/new-blog-author.yml b/.github/ISSUE_TEMPLATE/new-blog-author.yml index 16a1bac..bbfea34 100644 --- a/.github/ISSUE_TEMPLATE/new-blog-author.yml +++ b/.github/ISSUE_TEMPLATE/new-blog-author.yml @@ -1,5 +1,5 @@ name: New Blog Author -description: Add a new author to the blog authors list +description: "Add a new author to the blog authors list" title: "[New Author]: {INSERT NAME HERE}" labels: ["blog", "new-author"] body: diff --git a/.github/ISSUE_TEMPLATE/new-blog-post.yml b/.github/ISSUE_TEMPLATE/new-blog-post.yml index 9c63f6f..8535f1c 100644 --- a/.github/ISSUE_TEMPLATE/new-blog-post.yml +++ b/.github/ISSUE_TEMPLATE/new-blog-post.yml @@ -1,30 +1,56 @@ name: New Blog Post -description: Add a new post to the blog +description: "Add a new post to the blog" title: "[Blog Post]: " -labels: - - blog -assignees: - - shoverbj +labels: ["blog"] body: - type: input id: title attributes: - label: Blog Title - description: "Give a title to your blog post. Typically this would be - the name of the event/campout/etc." + label: "Blog Title" + description: "Give a title to your blog post. Typically this would be the name of the event/campout/etc." validations: required: true - type: dropdown id: authors attributes: - label: Select authors (use new author issue if the author isn't listed - here) + label: "Select authors" + description: "Use new author issue form if the author isn't listed here" multiple: true options: # AUTHOR_START - - Benjamin S + - Christopher Koczan + - Benjamin Shover # AUTHOR_END + validations: + required: true - type: checkboxes - id: - - \ No newline at end of file + id: tags + attributes: + label: Unit + description: "Select the unit(s) to associate this post with" + options: + - label: Troop 303 + - label: Troop 331 + - label: Crew 303 + - label: Pack 303 + validations: + required: true + - type: upload + id: cover-photo + attributes: + label: "Cover Photo" + description: "Photo shown at top of your blog post and on the homepage card" + validations: + required: false + accept: ".png, .jpg, .jpeg, .gif, .svg, .webp" + - type: upload + id: photo-album + attributes: + label: "Photo Album" + description: "Compress folder of photos as a zip file before uploading. If the file is too big (>25 Mb), the photo album will need to be added manually (see Mr. Shover)" + validations: + required: false + accept: ".zip" + - type: markdown + attributes: + value: "Insert blog text here!" From c35c9ca2497cc4688bcaadb0a626d257ca8179f3 Mon Sep 17 00:00:00 2001 From: shoverbj Date: Thu, 11 Jun 2026 14:54:35 -0400 Subject: [PATCH 07/50] add blog post automation --- .github/ISSUE_TEMPLATE/new-blog-author.yml | 2 +- .github/ISSUE_TEMPLATE/new-blog-post.yml | 4 +- .github/workflows/create-blog-post.yml | 224 ++++++++++++++++++ ...ex.mdx => 2026-04-27-western-camporee.mdx} | 4 +- .../index.mdx => 2026-04-28-railroading.mdx} | 2 +- .../index.mdx => 2026-06-03-spring-coh.mdx} | 2 +- ...x.mdx => 2026-06-09-scoutmasters-blog.mdx} | 0 ...x.mdx => 2026-06-10-scoutmasters-blog.mdx} | 0 src/components/BlogCard/index.jsx | 26 +- .../cover.webp | Bin .../img_013fef5abf5cdd088356d7af9adac652.webp | Bin .../img_01990c3226c4e1ae4ceaddd9a4f2672b.webp | Bin .../img_073e8504da382ea90468577df82de184.webp | Bin .../img_0ce5d3a55b98b702d236281a8d14405d.webp | Bin .../img_118eb750e1204cfb30981359f47b5d1f.webp | Bin .../img_127ea10820f69c22a95ffd07148406cb.webp | Bin .../img_160c160a5bc3fe7a12be026d9aaa3480.webp | Bin .../img_18242e9e4817d21ec181ca6d3193b31d.webp | Bin .../img_1bc017383fee9b2f4becb866d5f1bc86.webp | Bin .../img_20eb59725a6d4370b752c8335e0276ba.webp | Bin .../img_2234e8a0d926b1bfe1f09077e5ab6d23.webp | Bin .../img_229ccf36606d21543c2d93f94f4eeb4a.webp | Bin .../img_24386d6b4a90c1efc8a6865437385060.webp | Bin .../img_25d173a6fd4111e424dd8be27165db57.webp | Bin .../img_2a2f8a3ccc3384d31a43926d661c152b.webp | Bin .../img_2b12a5b2fe30b2bb8667e46b300fc18e.webp | Bin .../img_2c4f91f9278f84aa440dcb41e7ba98ce.webp | Bin .../img_2ec8b79ccfd60d279fb18e2b2c4ceea7.webp | Bin .../img_369146c3d46c9594c0b453b7255fd99d.webp | Bin .../img_429af1aa1d307f93e61aeb56b2addd38.webp | Bin .../img_462f39ce2a3cbdd4badd22ea01a77a0c.webp | Bin .../img_49693ee55581da5d315d10934d906aa6.webp | Bin .../img_499ce4ac9f8742941081f9d70e97f24e.webp | Bin .../img_55faf85cc491623f46189c5f39212b27.webp | Bin .../img_5ce066561a4e42abc16c72a63f91d256.webp | Bin .../img_5d269c1bf8e1462801c6e257fa95a23b.webp | Bin .../img_5f0e23c899ad85e9b8beea89006b6ceb.webp | Bin .../img_6555d07046f1937c9b2ce705d93cfe7c.webp | Bin .../img_65e4c2c90e50933ef9d94bb3cbc29fdf.webp | Bin .../img_66bfe2b80f6b96c80badef3970f5e4df.webp | Bin .../img_67d30a5b4dff25534457d956ca402442.webp | Bin .../img_7131f339cbbfe6c723dd9c74ab26bbd0.webp | Bin .../img_727f945328533dee902c0f7b426efbcd.webp | Bin .../img_739dfdc1ce91cc17e1fec1d44d167078.webp | Bin .../img_740277913e868852068dfb86b0d40a30.webp | Bin .../img_7828701f73540a10cb5db195bf35a6f0.webp | Bin .../img_800cb3da2dace0b36c64619209b2c71d.webp | Bin .../img_8289c181a3fdee5ef28bd0c2e51d79a7.webp | Bin .../img_8b88162a76240f4687b518d3dd8ebe1f.webp | Bin .../img_9da35a4b463f6234e29c0c285dcc4b56.webp | Bin .../img_b40d70735aa8f402071428eec63928dc.webp | Bin .../img_d374132f295a1c926c65882a2dd7f17e.webp | Bin .../img_d3e10c27b5314fef550059f1a4d5b6f8.webp | Bin .../img_d8c95f9bb7ba8666dc8ac2a0e856a536.webp | Bin .../img_d96608452ea99f17cb17f2869ba1c304.webp | Bin .../img_da1e038257b56f5fab0da353041ebdc2.webp | Bin .../img_dda1d364e17624b720bae6f202613750.webp | Bin .../img_ed0f3fc8a60cac317928ad3d265b8e88.webp | Bin .../img_f06e89f8298648f1686e40d2895747fd.webp | Bin .../img_f37750dc9e3076ac91500ac42f0b770e.webp | Bin .../img_f5f2fdd804084154a61eea6c9a7edcd1.webp | Bin .../img_f97b5cb4530e08a41bb1a84423b8b745.webp | Bin .../img_fa670e8c84600faa35b89fe1a21af847.webp | Bin .../img_fe2eb6ca68f30fedf2d4dbc33cea4011.webp | Bin .../cover.webp | Bin .../img_0bf23f9297cdc7df10828adec0c6876b.webp | Bin .../img_0e4304bcbf04824aa619f1ac5f853f1a.webp | Bin .../img_112c58f4a942bcbc7cec89408d5e4137.webp | Bin .../img_16ad97cd33527b8814a4a390e080965e.webp | Bin .../img_175cd2b81ca2f9e5ae3289d12f8fdac0.webp | Bin .../img_1a29143af78ee8cda8fdfea349ae4986.webp | Bin .../img_22106266dac5bd6be214e081e4ebe70b.webp | Bin .../img_25cd67b0323ae55ab2759a58634c1a0e.webp | Bin .../img_2b9ea0b338938c5b762a8f4b92dfbf2b.webp | Bin .../img_2e54d6fc9ab3b904c17d23ebb5d163ca.webp | Bin .../img_33477578c1088d8612a0b3c9d42281ff.webp | Bin .../img_36323605f8524ce2b7480d69dda6c809.webp | Bin .../img_37951f6bb99059b496ed398558cf4be0.webp | Bin .../img_3abb23abe23b978358115b855db3331f.webp | Bin .../img_4213d3e6656d53cd68d626e131acfa27.webp | Bin .../img_43dd7e2a11481e1b4c1847af3488c2fd.webp | Bin .../img_4beb620c869736348ac38cb2d92b3edd.webp | Bin .../img_58c7e351445a385d68dae7360a53a999.webp | Bin .../img_5c84446556ecc06ac0d5c30f39b7beeb.webp | Bin .../img_6688f0b8e9f5b04c5e1ca2a3c2ab9884.webp | Bin .../img_6839726ce37bc8c4425ed331a1da064b.webp | Bin .../img_702cd488ae42e073f5919637ee3f119e.webp | Bin .../img_706cb5154f958fde43b3ad9cf607cd2d.webp | Bin .../img_73a6a7e97f17a023e4e8d67a5e5a2165.webp | Bin .../img_7917a4a47564671480828617dc3f9bdb.webp | Bin .../img_7984bf307ddefb726363699fa5ee0215.webp | Bin .../img_7c16465fa71d97cb07d8b8b2ca4ad3a4.webp | Bin .../img_88ccf92a485df947164d1f147b8e30fa.webp | Bin .../img_8de3e72bc70a83124e3f5cbb92d109a7.webp | Bin .../img_8efb4e69a792c5d2d7eba790099f8fdd.webp | Bin .../img_9b1d6562ba88607d3c8f4614f01d4ee5.webp | Bin .../img_9d444d69318c6f8976495a781c34f51f.webp | Bin .../img_b413603c00f6c0cc75b4abc53a3c57d6.webp | Bin .../img_b98c161b3acdf2462997be9e2afba9d5.webp | Bin .../img_c44b4f3d31f77ebcd1d74f5afb4eacbb.webp | Bin .../img_c66e0b9e99085cb0f0d3a754edbd458a.webp | Bin .../img_cc2dcc7bd0831e745ea9c921186ce208.webp | Bin .../img_d0f1d9ce3d164a21944d956b3e703ab2.webp | Bin .../img_dc1fa78d869170cce144a3c3f0c2d2f8.webp | Bin .../img_e7ebd3282288f0ffbe36b2290f289fc7.webp | Bin .../img_f1274c2e7cffd24cea0c30488adf75bc.webp | Bin .../img_f2cd388605f026116e0b643c53ade0a5.webp | Bin .../img_f49c35787e07cf4fafd790936c44afb6.webp | Bin .../img_ff2c367d2a477bf40e879879b96209e7.webp | Bin 109 files changed, 252 insertions(+), 12 deletions(-) create mode 100644 .github/workflows/create-blog-post.yml rename blog/{2026/04/27/western-camporee/index.mdx => 2026-04-27-western-camporee.mdx} (91%) rename blog/{2026/04/28/railroading/index.mdx => 2026-04-28-railroading.mdx} (82%) rename blog/{2026/06/03/spring-coh/index.mdx => 2026-06-03-spring-coh.mdx} (92%) rename blog/{2026/06/09/scoutmaster-blog/index.mdx => 2026-06-09-scoutmasters-blog.mdx} (100%) rename blog/{2026/06/10/scoutmaster-blog/index.mdx => 2026-06-10-scoutmasters-blog.mdx} (100%) rename static/img/blog/{2026/04/27/western-camporee => 2026-04-27-western-camporee}/cover.webp (100%) rename static/img/blog/{2026/04/27/western-camporee => 2026-04-27-western-camporee}/slides/img_013fef5abf5cdd088356d7af9adac652.webp (100%) rename static/img/blog/{2026/04/27/western-camporee => 2026-04-27-western-camporee}/slides/img_01990c3226c4e1ae4ceaddd9a4f2672b.webp (100%) rename static/img/blog/{2026/04/27/western-camporee => 2026-04-27-western-camporee}/slides/img_073e8504da382ea90468577df82de184.webp (100%) rename static/img/blog/{2026/04/27/western-camporee => 2026-04-27-western-camporee}/slides/img_0ce5d3a55b98b702d236281a8d14405d.webp (100%) rename static/img/blog/{2026/04/27/western-camporee => 2026-04-27-western-camporee}/slides/img_118eb750e1204cfb30981359f47b5d1f.webp (100%) rename static/img/blog/{2026/04/27/western-camporee => 2026-04-27-western-camporee}/slides/img_127ea10820f69c22a95ffd07148406cb.webp (100%) rename static/img/blog/{2026/04/27/western-camporee => 2026-04-27-western-camporee}/slides/img_160c160a5bc3fe7a12be026d9aaa3480.webp (100%) rename static/img/blog/{2026/04/27/western-camporee => 2026-04-27-western-camporee}/slides/img_18242e9e4817d21ec181ca6d3193b31d.webp (100%) rename static/img/blog/{2026/04/27/western-camporee => 2026-04-27-western-camporee}/slides/img_1bc017383fee9b2f4becb866d5f1bc86.webp (100%) rename static/img/blog/{2026/04/27/western-camporee => 2026-04-27-western-camporee}/slides/img_20eb59725a6d4370b752c8335e0276ba.webp (100%) rename static/img/blog/{2026/04/27/western-camporee => 2026-04-27-western-camporee}/slides/img_2234e8a0d926b1bfe1f09077e5ab6d23.webp (100%) rename static/img/blog/{2026/04/27/western-camporee => 2026-04-27-western-camporee}/slides/img_229ccf36606d21543c2d93f94f4eeb4a.webp (100%) rename static/img/blog/{2026/04/27/western-camporee => 2026-04-27-western-camporee}/slides/img_24386d6b4a90c1efc8a6865437385060.webp (100%) rename static/img/blog/{2026/04/27/western-camporee => 2026-04-27-western-camporee}/slides/img_25d173a6fd4111e424dd8be27165db57.webp (100%) rename static/img/blog/{2026/04/27/western-camporee => 2026-04-27-western-camporee}/slides/img_2a2f8a3ccc3384d31a43926d661c152b.webp (100%) rename static/img/blog/{2026/04/27/western-camporee => 2026-04-27-western-camporee}/slides/img_2b12a5b2fe30b2bb8667e46b300fc18e.webp (100%) rename static/img/blog/{2026/04/27/western-camporee => 2026-04-27-western-camporee}/slides/img_2c4f91f9278f84aa440dcb41e7ba98ce.webp (100%) rename static/img/blog/{2026/04/27/western-camporee => 2026-04-27-western-camporee}/slides/img_2ec8b79ccfd60d279fb18e2b2c4ceea7.webp (100%) rename static/img/blog/{2026/04/27/western-camporee => 2026-04-27-western-camporee}/slides/img_369146c3d46c9594c0b453b7255fd99d.webp (100%) rename static/img/blog/{2026/04/27/western-camporee => 2026-04-27-western-camporee}/slides/img_429af1aa1d307f93e61aeb56b2addd38.webp (100%) rename static/img/blog/{2026/04/27/western-camporee => 2026-04-27-western-camporee}/slides/img_462f39ce2a3cbdd4badd22ea01a77a0c.webp (100%) rename static/img/blog/{2026/04/27/western-camporee => 2026-04-27-western-camporee}/slides/img_49693ee55581da5d315d10934d906aa6.webp (100%) rename static/img/blog/{2026/04/27/western-camporee => 2026-04-27-western-camporee}/slides/img_499ce4ac9f8742941081f9d70e97f24e.webp (100%) rename static/img/blog/{2026/04/27/western-camporee => 2026-04-27-western-camporee}/slides/img_55faf85cc491623f46189c5f39212b27.webp (100%) rename static/img/blog/{2026/04/27/western-camporee => 2026-04-27-western-camporee}/slides/img_5ce066561a4e42abc16c72a63f91d256.webp (100%) rename static/img/blog/{2026/04/27/western-camporee => 2026-04-27-western-camporee}/slides/img_5d269c1bf8e1462801c6e257fa95a23b.webp (100%) rename static/img/blog/{2026/04/27/western-camporee => 2026-04-27-western-camporee}/slides/img_5f0e23c899ad85e9b8beea89006b6ceb.webp (100%) rename static/img/blog/{2026/04/27/western-camporee => 2026-04-27-western-camporee}/slides/img_6555d07046f1937c9b2ce705d93cfe7c.webp (100%) rename static/img/blog/{2026/04/27/western-camporee => 2026-04-27-western-camporee}/slides/img_65e4c2c90e50933ef9d94bb3cbc29fdf.webp (100%) rename static/img/blog/{2026/04/27/western-camporee => 2026-04-27-western-camporee}/slides/img_66bfe2b80f6b96c80badef3970f5e4df.webp (100%) rename static/img/blog/{2026/04/27/western-camporee => 2026-04-27-western-camporee}/slides/img_67d30a5b4dff25534457d956ca402442.webp (100%) rename static/img/blog/{2026/04/27/western-camporee => 2026-04-27-western-camporee}/slides/img_7131f339cbbfe6c723dd9c74ab26bbd0.webp (100%) rename static/img/blog/{2026/04/27/western-camporee => 2026-04-27-western-camporee}/slides/img_727f945328533dee902c0f7b426efbcd.webp (100%) rename static/img/blog/{2026/04/27/western-camporee => 2026-04-27-western-camporee}/slides/img_739dfdc1ce91cc17e1fec1d44d167078.webp (100%) rename static/img/blog/{2026/04/27/western-camporee => 2026-04-27-western-camporee}/slides/img_740277913e868852068dfb86b0d40a30.webp (100%) rename static/img/blog/{2026/04/27/western-camporee => 2026-04-27-western-camporee}/slides/img_7828701f73540a10cb5db195bf35a6f0.webp (100%) rename static/img/blog/{2026/04/27/western-camporee => 2026-04-27-western-camporee}/slides/img_800cb3da2dace0b36c64619209b2c71d.webp (100%) rename static/img/blog/{2026/04/27/western-camporee => 2026-04-27-western-camporee}/slides/img_8289c181a3fdee5ef28bd0c2e51d79a7.webp (100%) rename static/img/blog/{2026/04/27/western-camporee => 2026-04-27-western-camporee}/slides/img_8b88162a76240f4687b518d3dd8ebe1f.webp (100%) rename static/img/blog/{2026/04/27/western-camporee => 2026-04-27-western-camporee}/slides/img_9da35a4b463f6234e29c0c285dcc4b56.webp (100%) rename static/img/blog/{2026/04/27/western-camporee => 2026-04-27-western-camporee}/slides/img_b40d70735aa8f402071428eec63928dc.webp (100%) rename static/img/blog/{2026/04/27/western-camporee => 2026-04-27-western-camporee}/slides/img_d374132f295a1c926c65882a2dd7f17e.webp (100%) rename static/img/blog/{2026/04/27/western-camporee => 2026-04-27-western-camporee}/slides/img_d3e10c27b5314fef550059f1a4d5b6f8.webp (100%) rename static/img/blog/{2026/04/27/western-camporee => 2026-04-27-western-camporee}/slides/img_d8c95f9bb7ba8666dc8ac2a0e856a536.webp (100%) rename static/img/blog/{2026/04/27/western-camporee => 2026-04-27-western-camporee}/slides/img_d96608452ea99f17cb17f2869ba1c304.webp (100%) rename static/img/blog/{2026/04/27/western-camporee => 2026-04-27-western-camporee}/slides/img_da1e038257b56f5fab0da353041ebdc2.webp (100%) rename static/img/blog/{2026/04/27/western-camporee => 2026-04-27-western-camporee}/slides/img_dda1d364e17624b720bae6f202613750.webp (100%) rename static/img/blog/{2026/04/27/western-camporee => 2026-04-27-western-camporee}/slides/img_ed0f3fc8a60cac317928ad3d265b8e88.webp (100%) rename static/img/blog/{2026/04/27/western-camporee => 2026-04-27-western-camporee}/slides/img_f06e89f8298648f1686e40d2895747fd.webp (100%) rename static/img/blog/{2026/04/27/western-camporee => 2026-04-27-western-camporee}/slides/img_f37750dc9e3076ac91500ac42f0b770e.webp (100%) rename static/img/blog/{2026/04/27/western-camporee => 2026-04-27-western-camporee}/slides/img_f5f2fdd804084154a61eea6c9a7edcd1.webp (100%) rename static/img/blog/{2026/04/27/western-camporee => 2026-04-27-western-camporee}/slides/img_f97b5cb4530e08a41bb1a84423b8b745.webp (100%) rename static/img/blog/{2026/04/27/western-camporee => 2026-04-27-western-camporee}/slides/img_fa670e8c84600faa35b89fe1a21af847.webp (100%) rename static/img/blog/{2026/04/27/western-camporee => 2026-04-27-western-camporee}/slides/img_fe2eb6ca68f30fedf2d4dbc33cea4011.webp (100%) rename static/img/blog/{2026/04/28/railroading => 2026-04-28-railroading}/cover.webp (100%) rename static/img/blog/{2026/06/03/spring-coh => 2026-06-03-spring-coh}/slides/img_0bf23f9297cdc7df10828adec0c6876b.webp (100%) rename static/img/blog/{2026/06/03/spring-coh => 2026-06-03-spring-coh}/slides/img_0e4304bcbf04824aa619f1ac5f853f1a.webp (100%) rename static/img/blog/{2026/06/03/spring-coh => 2026-06-03-spring-coh}/slides/img_112c58f4a942bcbc7cec89408d5e4137.webp (100%) rename static/img/blog/{2026/06/03/spring-coh => 2026-06-03-spring-coh}/slides/img_16ad97cd33527b8814a4a390e080965e.webp (100%) rename static/img/blog/{2026/06/03/spring-coh => 2026-06-03-spring-coh}/slides/img_175cd2b81ca2f9e5ae3289d12f8fdac0.webp (100%) rename static/img/blog/{2026/06/03/spring-coh => 2026-06-03-spring-coh}/slides/img_1a29143af78ee8cda8fdfea349ae4986.webp (100%) rename static/img/blog/{2026/06/03/spring-coh => 2026-06-03-spring-coh}/slides/img_22106266dac5bd6be214e081e4ebe70b.webp (100%) rename static/img/blog/{2026/06/03/spring-coh => 2026-06-03-spring-coh}/slides/img_25cd67b0323ae55ab2759a58634c1a0e.webp (100%) rename static/img/blog/{2026/06/03/spring-coh => 2026-06-03-spring-coh}/slides/img_2b9ea0b338938c5b762a8f4b92dfbf2b.webp (100%) rename static/img/blog/{2026/06/03/spring-coh => 2026-06-03-spring-coh}/slides/img_2e54d6fc9ab3b904c17d23ebb5d163ca.webp (100%) rename static/img/blog/{2026/06/03/spring-coh => 2026-06-03-spring-coh}/slides/img_33477578c1088d8612a0b3c9d42281ff.webp (100%) rename static/img/blog/{2026/06/03/spring-coh => 2026-06-03-spring-coh}/slides/img_36323605f8524ce2b7480d69dda6c809.webp (100%) rename static/img/blog/{2026/06/03/spring-coh => 2026-06-03-spring-coh}/slides/img_37951f6bb99059b496ed398558cf4be0.webp (100%) rename static/img/blog/{2026/06/03/spring-coh => 2026-06-03-spring-coh}/slides/img_3abb23abe23b978358115b855db3331f.webp (100%) rename static/img/blog/{2026/06/03/spring-coh => 2026-06-03-spring-coh}/slides/img_4213d3e6656d53cd68d626e131acfa27.webp (100%) rename static/img/blog/{2026/06/03/spring-coh => 2026-06-03-spring-coh}/slides/img_43dd7e2a11481e1b4c1847af3488c2fd.webp (100%) rename static/img/blog/{2026/06/03/spring-coh => 2026-06-03-spring-coh}/slides/img_4beb620c869736348ac38cb2d92b3edd.webp (100%) rename static/img/blog/{2026/06/03/spring-coh => 2026-06-03-spring-coh}/slides/img_58c7e351445a385d68dae7360a53a999.webp (100%) rename static/img/blog/{2026/06/03/spring-coh => 2026-06-03-spring-coh}/slides/img_5c84446556ecc06ac0d5c30f39b7beeb.webp (100%) rename static/img/blog/{2026/06/03/spring-coh => 2026-06-03-spring-coh}/slides/img_6688f0b8e9f5b04c5e1ca2a3c2ab9884.webp (100%) rename static/img/blog/{2026/06/03/spring-coh => 2026-06-03-spring-coh}/slides/img_6839726ce37bc8c4425ed331a1da064b.webp (100%) rename static/img/blog/{2026/06/03/spring-coh => 2026-06-03-spring-coh}/slides/img_702cd488ae42e073f5919637ee3f119e.webp (100%) rename static/img/blog/{2026/06/03/spring-coh => 2026-06-03-spring-coh}/slides/img_706cb5154f958fde43b3ad9cf607cd2d.webp (100%) rename static/img/blog/{2026/06/03/spring-coh => 2026-06-03-spring-coh}/slides/img_73a6a7e97f17a023e4e8d67a5e5a2165.webp (100%) rename static/img/blog/{2026/06/03/spring-coh => 2026-06-03-spring-coh}/slides/img_7917a4a47564671480828617dc3f9bdb.webp (100%) rename static/img/blog/{2026/06/03/spring-coh => 2026-06-03-spring-coh}/slides/img_7984bf307ddefb726363699fa5ee0215.webp (100%) rename static/img/blog/{2026/06/03/spring-coh => 2026-06-03-spring-coh}/slides/img_7c16465fa71d97cb07d8b8b2ca4ad3a4.webp (100%) rename static/img/blog/{2026/06/03/spring-coh => 2026-06-03-spring-coh}/slides/img_88ccf92a485df947164d1f147b8e30fa.webp (100%) rename static/img/blog/{2026/06/03/spring-coh => 2026-06-03-spring-coh}/slides/img_8de3e72bc70a83124e3f5cbb92d109a7.webp (100%) rename static/img/blog/{2026/06/03/spring-coh => 2026-06-03-spring-coh}/slides/img_8efb4e69a792c5d2d7eba790099f8fdd.webp (100%) rename static/img/blog/{2026/06/03/spring-coh => 2026-06-03-spring-coh}/slides/img_9b1d6562ba88607d3c8f4614f01d4ee5.webp (100%) rename static/img/blog/{2026/06/03/spring-coh => 2026-06-03-spring-coh}/slides/img_9d444d69318c6f8976495a781c34f51f.webp (100%) rename static/img/blog/{2026/06/03/spring-coh => 2026-06-03-spring-coh}/slides/img_b413603c00f6c0cc75b4abc53a3c57d6.webp (100%) rename static/img/blog/{2026/06/03/spring-coh => 2026-06-03-spring-coh}/slides/img_b98c161b3acdf2462997be9e2afba9d5.webp (100%) rename static/img/blog/{2026/06/03/spring-coh => 2026-06-03-spring-coh}/slides/img_c44b4f3d31f77ebcd1d74f5afb4eacbb.webp (100%) rename static/img/blog/{2026/06/03/spring-coh => 2026-06-03-spring-coh}/slides/img_c66e0b9e99085cb0f0d3a754edbd458a.webp (100%) rename static/img/blog/{2026/06/03/spring-coh => 2026-06-03-spring-coh}/slides/img_cc2dcc7bd0831e745ea9c921186ce208.webp (100%) rename static/img/blog/{2026/06/03/spring-coh => 2026-06-03-spring-coh}/slides/img_d0f1d9ce3d164a21944d956b3e703ab2.webp (100%) rename static/img/blog/{2026/06/03/spring-coh => 2026-06-03-spring-coh}/slides/img_dc1fa78d869170cce144a3c3f0c2d2f8.webp (100%) rename static/img/blog/{2026/06/03/spring-coh => 2026-06-03-spring-coh}/slides/img_e7ebd3282288f0ffbe36b2290f289fc7.webp (100%) rename static/img/blog/{2026/06/03/spring-coh => 2026-06-03-spring-coh}/slides/img_f1274c2e7cffd24cea0c30488adf75bc.webp (100%) rename static/img/blog/{2026/06/03/spring-coh => 2026-06-03-spring-coh}/slides/img_f2cd388605f026116e0b643c53ade0a5.webp (100%) rename static/img/blog/{2026/06/03/spring-coh => 2026-06-03-spring-coh}/slides/img_f49c35787e07cf4fafd790936c44afb6.webp (100%) rename static/img/blog/{2026/06/03/spring-coh => 2026-06-03-spring-coh}/slides/img_ff2c367d2a477bf40e879879b96209e7.webp (100%) diff --git a/.github/ISSUE_TEMPLATE/new-blog-author.yml b/.github/ISSUE_TEMPLATE/new-blog-author.yml index bbfea34..75742ea 100644 --- a/.github/ISSUE_TEMPLATE/new-blog-author.yml +++ b/.github/ISSUE_TEMPLATE/new-blog-author.yml @@ -1,7 +1,7 @@ name: New Blog Author description: "Add a new author to the blog authors list" title: "[New Author]: {INSERT NAME HERE}" -labels: ["blog", "new-author"] +labels: ["new-author"] body: - type: input id: name diff --git a/.github/ISSUE_TEMPLATE/new-blog-post.yml b/.github/ISSUE_TEMPLATE/new-blog-post.yml index 8535f1c..d59a51d 100644 --- a/.github/ISSUE_TEMPLATE/new-blog-post.yml +++ b/.github/ISSUE_TEMPLATE/new-blog-post.yml @@ -52,5 +52,5 @@ body: required: false accept: ".zip" - type: markdown - attributes: - value: "Insert blog text here!" + attributes: + value: "Insert blog text here!" diff --git a/.github/workflows/create-blog-post.yml b/.github/workflows/create-blog-post.yml new file mode 100644 index 0000000..b00872e --- /dev/null +++ b/.github/workflows/create-blog-post.yml @@ -0,0 +1,224 @@ +name: Generate Blog Post from Issue + +on: + issues: + types: [opened] + +jobs: + create-post: + if: contains(github.event.issue.labels.*.name, 'blog') + runs-on: ubuntu-latest + + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Generate GitHub App Token + id: app-token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ secrets.YOUR_EXISTING_APP_ID }} + private-key: ${{ secrets.YOUR_EXISTING_APP_PRIVATE_KEY }} + + - name: Parse Issue Form + id: parse + uses: stefanbuck/github-issue-parser@v3 + with: + template-path: .github/ISSUE_TEMPLATE/new-blog-post.yml + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.x' + + # Install Pillow dependency required by your image optimizer script + - name: Install Dependencies + run: | + python -m pip install --upgrade pip + pip install Pillow + + # Process issue text, staging assets, optimization steps, and build output + - name: Process Issue Data and Assets (Python Optimizer) + id: process + env: + ISSUE_JSON: ${{ steps.parse.outputs.jsonString }} + RAW_BODY: ${{ github.event.issue.body }} + run: | + python -c ' + import os + import json + import re + import hashlib + import urllib.request + import zipfile + from io import BytesIO + from datetime import datetime + from PIL import Image, ImageOps + + # --- 1. INTEGRATED OPTIMIZER ENGINE CODE --- + def optimize_convert_and_hash_images(input_dir, output_dir, max_size=(1920, 1080), quality=80, keep_original_names=False): + if not os.path.exists(input_dir): + return + os.makedirs(output_dir, exist_ok=True) + valid_extensions = (".jpg", ".jpeg", ".png") + for root, _, files in os.walk(input_dir): + for file in files: + if file.lower().endswith(valid_extensions): + input_path = os.path.join(root, file) + relative_path = os.path.relpath(root, input_dir) + target_folder = os.path.normpath(os.path.join(output_dir, relative_path)) + os.makedirs(target_folder, exist_ok=True) + try: + with Image.open(input_path) as img: + img = ImageOps.exif_transpose(img) + if img.mode in ("P", "CMYK"): + img = img.convert("RGBA") + img.thumbnail(max_size, Image.Resampling.LANCZOS) + buffer = BytesIO() + img.save(buffer, format="WEBP", quality=quality) + optimized_data = buffer.getvalue() + if keep_original_names: + base_name, _ = os.path.splitext(file) + output_file_name = f"{base_name}.webp" + else: + hasher = hashlib.md5(optimized_data) + content_hash = hasher.hexdigest() + output_file_name = f"img_{content_hash}.webp" + output_path = os.path.join(target_folder, output_file_name) + if os.path.exists(output_path): + os.remove(input_path) + continue + with open(output_path, "wb") as f: + f.write(optimized_data) + if os.path.exists(output_path) and os.path.getsize(output_path) > 0: + os.remove(input_path) + print(f"[Processed & Purged] {file} -> {output_file_name}") + except Exception as e: + print(f"Failed to process {file}. Error: {e}") + + # --- 2. SETUP FORMS & METADATA --- + data = json.loads(os.environ["ISSUE_JSON"]) + body_text = os.environ.get("RAW_BODY", "") + date_str = datetime.now().strftime("%Y-%m-%d") + + def to_kebab(text): + if not text: return "" + text = text.lower().strip() + text = re.sub(r"[^a-z0-9\s-]", "", text) + return re.sub(r"[\s-]+", "-", text).strip("-") + + slug = to_kebab(data.get("title", "untitled")) + blogfilename = f"{date_str}-{slug}" + + authors = [to_kebab(a) for a in data.get("authors", [])] + tags = [to_kebab(t["label"]) for t in data.get("tags", []) if t.get("value") == True] + + # Define Docusaurus target destinations + static_folder = f"static/img/blog/{blogfilename}" + web_prefix = f"/img/blog/{blogfilename}" + + # --- 3. FETCH AND RUN OPTIMIZER ON COVER PHOTO --- + cover_url = data.get("cover-photo") + cover_line = "" + if cover_url and cover_url != "null": + # Create an isolated staging bucket for the download + tmp_cover_dir = "/tmp/raw_cover" + os.makedirs(tmp_cover_dir, exist_ok=True) + + clean_url = cover_url.split("?")[0] + _, ext = os.path.splitext(clean_url) + if not ext: ext = ".jpg" + + # Stage raw image file natively + raw_cover_path = os.path.join(tmp_cover_dir, f"cover{ext}") + urllib.request.urlretrieve(cover_url, raw_cover_path) + + # Run optimizer engine on cover folder -> outputs straight to static directory root + optimize_convert_and_hash_images(tmp_cover_dir, static_folder, keep_original_names=True) + cover_line = f"![Cover Picture]({web_prefix}/cover.webp)\n\n" + + # --- 4. FETCH AND RUN OPTIMIZER ON ZIP ALBUM SLIDES --- + album_url = data.get("photo-album") + has_album = False + album_line = "" + if album_url and album_url != "null": + tmp_slides_dir = "/tmp/raw_slides" + os.makedirs(tmp_slides_dir, exist_ok=True) + + zip_path = "album.zip" + urllib.request.urlretrieve(album_url, zip_path) + with zipfile.ZipFile(zip_path, "r") as zip_ref: + zip_ref.extractall(tmp_slides_dir) + os.remove(zip_path) + + # Run optimizer engine on extracted slides folder -> outputs straight to slides target subdirectory + slides_target_folder = os.path.join(static_folder, "slides") + optimize_convert_and_hash_images(tmp_slides_dir, slides_target_folder, keep_original_names=True) + + has_album = True + album_line = f"\n\n" + + # --- 5. PARSE TEXT CONTENT --- + marker = "Insert blog text here!" + if marker in body_text: + body_text = body_text.split(marker)[-1].strip() + + paragraphs = [p.strip() for p in body_text.split("\n\n") if p.strip()] + content_markdown = "" + if paragraphs: + content_markdown += paragraphs[0] + "\n\n{/* truncate */}" + if len(paragraphs) > 1: + content_markdown += "\n\n" + "\n\n".join(paragraphs[1:]) + + # --- 6. COMPILE INTEGRATED FILE TEXT --- + authors_str = ", ".join(authors) + tags_str = ", ".join(tags) + import_line = "import PhotoAlbumGallery from '\''@site/src/components/PhotoAlbumGallery'\'';\n\n" if has_album else "" + + file_content = f"""--- +title: {data.get("title")} +authors: [{authors_str}] +tags: [{tags_str}] +--- + +{import_line}{cover_line}{content_markdown}{album_line} +""" + os.makedirs("blog", exist_ok=True) + with open(f"blog/{blogfilename}.md", "w", encoding="utf-8") as f: + f.write(file_content.strip() + "\n") + + with open(os.environ["GITHUB_ENV"], "a") as env_file: + env_file.write(f"BLOG_FILENAME={blogfilename}\n") + ' + + - name: Create Pull Request + id: cpr + uses: peter-evans/create-pull-request@v6 + with: + token: ${{ steps.app-token.outputs.token }} + commit-message: "feat(blog): add new post from issue #${{ github.event.issue.number }}" + branch: "blog/issue-${{ github.event.issue.number }}-${{ env.BLOG_FILENAME }}" + title: "feat(blog): ${{ github.event.issue.title }}" + body: | + This Pull Request was automatically generated from Issue #${{ github.event.issue.number }}. + + ### Asset Optimization Checklist: + - [x] Extracted raw upload fields safely + - [x] Auto-converted assets to heavily compressed **WebP** formats + - [x] Stripped EXIF data and resized frames to bounding `1920x1080` parameters + + Closes #${{ github.event.issue.number }} + labels: | + blog + ready-to-review + + - name: Comment on Issue with PR Link + if: steps.cpr.outputs.pull-request-number != '' + uses: peter-evans/create-or-update-comment@v4 + with: + token: ${{ steps.app-token.outputs.token }} + issue-number: ${{ github.event.issue.number }} + body: | + 🎉 **Success!** Your blog post has been generated and all assets have been fully optimized into WebP formats. + + Review your branch modifications and staging environments here:👉 ${{ steps.cpr.outputs.pull-request-url }} diff --git a/blog/2026/04/27/western-camporee/index.mdx b/blog/2026-04-27-western-camporee.mdx similarity index 91% rename from blog/2026/04/27/western-camporee/index.mdx rename to blog/2026-04-27-western-camporee.mdx index 569038f..8881855 100644 --- a/blog/2026/04/27/western-camporee/index.mdx +++ b/blog/2026-04-27-western-camporee.mdx @@ -6,7 +6,7 @@ tags: [troop-303] import PhotoAlbumGallery from '@site/src/components/PhotoAlbumGallery'; -![Departure Picture](/img/blog/2026/04/27/western-camporee/cover.webp) +![Departure Picture](/img/blog/2026-04-27-western-camporee/cover.webp) The Scouts of the Legendary Troop 303 attended the 2026 Western Division Spring Camporee this past weekend. @@ -33,5 +33,5 @@ Overall, a great weekend was had by all who attended and the troop looks forward to future adventures! diff --git a/blog/2026/04/28/railroading/index.mdx b/blog/2026-04-28-railroading.mdx similarity index 82% rename from blog/2026/04/28/railroading/index.mdx rename to blog/2026-04-28-railroading.mdx index adf7b51..66b94ea 100644 --- a/blog/2026/04/28/railroading/index.mdx +++ b/blog/2026-04-28-railroading.mdx @@ -4,7 +4,7 @@ authors: [benjamin-shover] tags: [troop-331] --- -![Blog Cover Photo](/img/blog/2026/04/28/railroading/cover.webp) +![Blog Cover Photo](/img/blog/2026-04-28-railroading/cover.webp) The Scouts of Troop 331 went out of council to attend the Hoosier Trails Council's Railroading Camporee. diff --git a/blog/2026/06/03/spring-coh/index.mdx b/blog/2026-06-03-spring-coh.mdx similarity index 92% rename from blog/2026/06/03/spring-coh/index.mdx rename to blog/2026-06-03-spring-coh.mdx index 09aacee..8ede0a3 100644 --- a/blog/2026/06/03/spring-coh/index.mdx +++ b/blog/2026-06-03-spring-coh.mdx @@ -21,5 +21,5 @@ Congrats to all of the Scouts who earned awards! Be sure to look through the image gallery to see all the highlights from the evening. diff --git a/blog/2026/06/09/scoutmaster-blog/index.mdx b/blog/2026-06-09-scoutmasters-blog.mdx similarity index 100% rename from blog/2026/06/09/scoutmaster-blog/index.mdx rename to blog/2026-06-09-scoutmasters-blog.mdx diff --git a/blog/2026/06/10/scoutmaster-blog/index.mdx b/blog/2026-06-10-scoutmasters-blog.mdx similarity index 100% rename from blog/2026/06/10/scoutmaster-blog/index.mdx rename to blog/2026-06-10-scoutmasters-blog.mdx diff --git a/src/components/BlogCard/index.jsx b/src/components/BlogCard/index.jsx index cb786ad..076c6d4 100644 --- a/src/components/BlogCard/index.jsx +++ b/src/components/BlogCard/index.jsx @@ -22,14 +22,19 @@ import recentPosts from '@site/.docusaurus/recent-posts.json'; import useBaseUrl from "@docusaurus/useBaseUrl"; /** - * Localizes an ISO date string to "MMM DD, YYYY" format (e.g., "Oct 24, 2026"). + * Localizes an ISO date string to "MMM DD, YYYY" using UTC to prevent timezone offsets. * @private * @param {string} isoString - The ISO date format string from Docusaurus metadata. * @returns {string} The formatted date. */ const formatDate = (isoString) => { const date = new Date(isoString); - return new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric', year: 'numeric' }).format(date); + return new Intl.DateTimeFormat('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric', + timeZone: 'UTC' + }).format(date); }; /** @@ -46,13 +51,23 @@ const formatDate = (isoString) => { function BlogCard({ permalink, title, date, authors, tags }) { const fallbackDefaultImage = useBaseUrl('img/blog/default-blog-cover.webp'); - // 1. Strip trailing slashes and get the folder name from permalink - const cleanPath = permalink.replace(/^\/|\/$/g, ''); + // 1. Isolate the base slug name by stripping the leading "/blog" and trailing slashes + const cleanUrl = permalink.replace(/^\/|\/$/g, '').replace(/^blog\//, ''); + + // 2. Isolate ONLY the final trailing title string + const slugName = cleanUrl.split('/').pop(); + + // 3. Extract the exact YYYY-MM-DD prefix from the raw ISO string directly without date manipulation + // An ISO timestamp starts with "YYYY-MM-DD", so we slice the first 10 characters + const datePrefix = date.slice(0, 10); + + // 4. Combine them into the exact format requested: YYYY-MM-DD-blogtitle + const folderName = `${datePrefix}-${slugName}`; let resolvedCoverUrl; try { // 2. Webpack looks inside the static folder during compile time - resolvedCoverUrl = require(`@site/static/img/${cleanPath}/cover.webp`).default; + resolvedCoverUrl = require(`@site/static/img/blog/${folderName}/cover.webp`).default; } catch (err) { // 3. If file doesn't exist, it instantly uses the fallback at build time resolvedCoverUrl = fallbackDefaultImage; @@ -63,6 +78,7 @@ function BlogCard({ permalink, title, date, authors, tags }) {

+ {folderName} {title} Date: Thu, 11 Jun 2026 15:06:32 -0400 Subject: [PATCH 08/50] updating templates --- .github/ISSUE_TEMPLATE/new-blog-author.yml | 2 +- .github/ISSUE_TEMPLATE/new-blog-post.yml | 22 +++++++++++++--------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/new-blog-author.yml b/.github/ISSUE_TEMPLATE/new-blog-author.yml index 75742ea..a731954 100644 --- a/.github/ISSUE_TEMPLATE/new-blog-author.yml +++ b/.github/ISSUE_TEMPLATE/new-blog-author.yml @@ -1,6 +1,6 @@ name: New Blog Author description: "Add a new author to the blog authors list" -title: "[New Author]: {INSERT NAME HERE}" +title: "Add New Author" labels: ["new-author"] body: - type: input diff --git a/.github/ISSUE_TEMPLATE/new-blog-post.yml b/.github/ISSUE_TEMPLATE/new-blog-post.yml index d59a51d..8925fe7 100644 --- a/.github/ISSUE_TEMPLATE/new-blog-post.yml +++ b/.github/ISSUE_TEMPLATE/new-blog-post.yml @@ -1,15 +1,15 @@ name: New Blog Post description: "Add a new post to the blog" -title: "[Blog Post]: " labels: ["blog"] body: - - type: input - id: title + - type: markdown attributes: - label: "Blog Title" - description: "Give a title to your blog post. Typically this would be the name of the event/campout/etc." - validations: - required: true + value: | + **The title above will be the title of your blog post** + + Fill out this form and click "Create" below to submit a new blog post. + After submitting your post, it will be reviewed by the webmaster before + being posted to the website. - type: dropdown id: authors attributes: @@ -51,6 +51,10 @@ body: validations: required: false accept: ".zip" - - type: markdown + - type: textarea + id: blog-content attributes: - value: "Insert blog text here!" + label: "Blog Text" + description: "Write your blog post here!" + validations: + required: true From 9ceb672fe4569fbaf5bb262df0e266aa909257ac Mon Sep 17 00:00:00 2001 From: shoverbj Date: Thu, 11 Jun 2026 15:13:31 -0400 Subject: [PATCH 09/50] updates to template --- .github/ISSUE_TEMPLATE/new-blog-post.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/new-blog-post.yml b/.github/ISSUE_TEMPLATE/new-blog-post.yml index 8925fe7..650b178 100644 --- a/.github/ISSUE_TEMPLATE/new-blog-post.yml +++ b/.github/ISSUE_TEMPLATE/new-blog-post.yml @@ -1,6 +1,7 @@ name: New Blog Post description: "Add a new post to the blog" labels: ["blog"] +title: "" body: - type: markdown attributes: @@ -56,5 +57,7 @@ body: attributes: label: "Blog Text" description: "Write your blog post here!" + placeholder: "Blog text here..." + render: text validations: required: true From 45fdde701f4ad00eb686d5b5ecd4fc9b346d2c93 Mon Sep 17 00:00:00 2001 From: shoverbj Date: Thu, 11 Jun 2026 15:15:14 -0400 Subject: [PATCH 10/50] Update template --- .github/ISSUE_TEMPLATE/new-blog-post.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/new-blog-post.yml b/.github/ISSUE_TEMPLATE/new-blog-post.yml index 650b178..bcb3b04 100644 --- a/.github/ISSUE_TEMPLATE/new-blog-post.yml +++ b/.github/ISSUE_TEMPLATE/new-blog-post.yml @@ -1,13 +1,15 @@ name: New Blog Post description: "Add a new post to the blog" labels: ["blog"] -title: "" +title: "Replace this with your blog title" body: - type: markdown attributes: value: | **The title above will be the title of your blog post** + Your title should highlight what the post is about (campout/event/etc.) + Fill out this form and click "Create" below to submit a new blog post. After submitting your post, it will be reviewed by the webmaster before being posted to the website. From 1ff26fd331fb9febc52cc97604b412632a4a3325 Mon Sep 17 00:00:00 2001 From: shoverbj Date: Thu, 11 Jun 2026 15:26:50 -0400 Subject: [PATCH 11/50] cleaning things up --- .github/workflows/create-blog-post.yml | 73 ++++++++++++-------------- 1 file changed, 35 insertions(+), 38 deletions(-) diff --git a/.github/workflows/create-blog-post.yml b/.github/workflows/create-blog-post.yml index b00872e..97a12d6 100644 --- a/.github/workflows/create-blog-post.yml +++ b/.github/workflows/create-blog-post.yml @@ -31,20 +31,18 @@ jobs: with: python-version: '3.x' - # Install Pillow dependency required by your image optimizer script - name: Install Dependencies run: | python -m pip install --upgrade pip pip install Pillow - # Process issue text, staging assets, optimization steps, and build output - name: Process Issue Data and Assets (Python Optimizer) id: process env: ISSUE_JSON: ${{ steps.parse.outputs.jsonString }} - RAW_BODY: ${{ github.event.issue.body }} + RAW_TITLE: ${{ github.event.issue.title }} + shell: python run: | - python -c ' import os import json import re @@ -98,7 +96,7 @@ jobs: # --- 2. SETUP FORMS & METADATA --- data = json.loads(os.environ["ISSUE_JSON"]) - body_text = os.environ.get("RAW_BODY", "") + raw_title = os.environ.get("RAW_TITLE", "Untitled Post") date_str = datetime.now().strftime("%Y-%m-%d") def to_kebab(text): @@ -107,13 +105,12 @@ jobs: text = re.sub(r"[^a-z0-9\s-]", "", text) return re.sub(r"[\s-]+", "-", text).strip("-") - slug = to_kebab(data.get("title", "untitled")) + slug = to_kebab(raw_title) blogfilename = f"{date_str}-{slug}" authors = [to_kebab(a) for a in data.get("authors", [])] tags = [to_kebab(t["label"]) for t in data.get("tags", []) if t.get("value") == True] - # Define Docusaurus target destinations static_folder = f"static/img/blog/{blogfilename}" web_prefix = f"/img/blog/{blogfilename}" @@ -121,21 +118,18 @@ jobs: cover_url = data.get("cover-photo") cover_line = "" if cover_url and cover_url != "null": - # Create an isolated staging bucket for the download tmp_cover_dir = "/tmp/raw_cover" os.makedirs(tmp_cover_dir, exist_ok=True) - clean_url = cover_url.split("?")[0] - _, ext = os.path.splitext(clean_url) + clean_url = cover_url.split("?") + _, ext = os.path.splitext(clean_url[0]) if not ext: ext = ".jpg" - # Stage raw image file natively raw_cover_path = os.path.join(tmp_cover_dir, f"cover{ext}") urllib.request.urlretrieve(cover_url, raw_cover_path) - # Run optimizer engine on cover folder -> outputs straight to static directory root optimize_convert_and_hash_images(tmp_cover_dir, static_folder, keep_original_names=True) - cover_line = f"![Cover Picture]({web_prefix}/cover.webp)\n\n" + cover_line = f"![Cover Picture]({web_prefix}/cover.webp)" # --- 4. FETCH AND RUN OPTIMIZER ON ZIP ALBUM SLIDES --- album_url = data.get("photo-album") @@ -151,18 +145,14 @@ jobs: zip_ref.extractall(tmp_slides_dir) os.remove(zip_path) - # Run optimizer engine on extracted slides folder -> outputs straight to slides target subdirectory slides_target_folder = os.path.join(static_folder, "slides") optimize_convert_and_hash_images(tmp_slides_dir, slides_target_folder, keep_original_names=True) has_album = True - album_line = f"\n\n" - - # --- 5. PARSE TEXT CONTENT --- - marker = "Insert blog text here!" - if marker in body_text: - body_text = body_text.split(marker)[-1].strip() + album_line = f"" + # --- 5. PARSE TEXT CONTENT FROM TEXTAREA --- + body_text = data.get("blog-content", "").strip() paragraphs = [p.strip() for p in body_text.split("\n\n") if p.strip()] content_markdown = "" if paragraphs: @@ -170,26 +160,37 @@ jobs: if len(paragraphs) > 1: content_markdown += "\n\n" + "\n\n".join(paragraphs[1:]) - # --- 6. COMPILE INTEGRATED FILE TEXT --- + # --- 6. COMPILE FILE VIA CLEAN SEQUENTIAL LIST AND \n JOINS --- authors_str = ", ".join(authors) tags_str = ", ".join(tags) - import_line = "import PhotoAlbumGallery from '\''@site/src/components/PhotoAlbumGallery'\'';\n\n" if has_album else "" + + lines = [ + "---", + f"title: {raw_title}", + f"authors: [{authors_str}]", + f"tags: [{tags_str}]", + "---", + "" + ] + + if has_album: + lines.append("import PhotoAlbumGallery from '@site/src/components/PhotoAlbumGallery';\n") + if cover_line: + lines.append(cover_line + "\n") + + lines.append(content_markdown) + + if has_album and album_line: + lines.append("\n" + album_line) - file_content = f"""--- -title: {data.get("title")} -authors: [{authors_str}] -tags: [{tags_str}] ---- + file_content = "\n".join(lines).strip() + "\n" -{import_line}{cover_line}{content_markdown}{album_line} -""" os.makedirs("blog", exist_ok=True) with open(f"blog/{blogfilename}.md", "w", encoding="utf-8") as f: - f.write(file_content.strip() + "\n") + f.write(file_content) with open(os.environ["GITHUB_ENV"], "a") as env_file: env_file.write(f"BLOG_FILENAME={blogfilename}\n") - ' - name: Create Pull Request id: cpr @@ -202,11 +203,6 @@ tags: [{tags_str}] body: | This Pull Request was automatically generated from Issue #${{ github.event.issue.number }}. - ### Asset Optimization Checklist: - - [x] Extracted raw upload fields safely - - [x] Auto-converted assets to heavily compressed **WebP** formats - - [x] Stripped EXIF data and resized frames to bounding `1920x1080` parameters - Closes #${{ github.event.issue.number }} labels: | blog @@ -219,6 +215,7 @@ tags: [{tags_str}] token: ${{ steps.app-token.outputs.token }} issue-number: ${{ github.event.issue.number }} body: | - 🎉 **Success!** Your blog post has been generated and all assets have been fully optimized into WebP formats. + 🎉 **Success!** A staging branch has been created. - Review your branch modifications and staging environments here:👉 ${{ steps.cpr.outputs.pull-request-url }} + Your blog post is ready for review here: + 👉 ${{ steps.cpr.outputs.pull-request-url }} From d288f38a09a636ae68f5da48c33e1447d0d42ff8 Mon Sep 17 00:00:00 2001 From: shoverbj Date: Thu, 11 Jun 2026 15:31:08 -0400 Subject: [PATCH 12/50] update token --- .github/workflows/create-blog-post.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/create-blog-post.yml b/.github/workflows/create-blog-post.yml index 97a12d6..8be5662 100644 --- a/.github/workflows/create-blog-post.yml +++ b/.github/workflows/create-blog-post.yml @@ -17,8 +17,8 @@ jobs: id: app-token uses: actions/create-github-app-token@v1 with: - app-id: ${{ secrets.YOUR_EXISTING_APP_ID }} - private-key: ${{ secrets.YOUR_EXISTING_APP_PRIVATE_KEY }} + app-id: ${{ secrets.APP_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} - name: Parse Issue Form id: parse From 01f01bcc8d59e73b265ae826f1c00697f01c4475 Mon Sep 17 00:00:00 2001 From: shoverbj Date: Fri, 12 Jun 2026 07:32:29 -0400 Subject: [PATCH 13/50] update workflow --- .github/workflows/create-blog-post.yml | 77 ++++++++++++++------------ 1 file changed, 42 insertions(+), 35 deletions(-) diff --git a/.github/workflows/create-blog-post.yml b/.github/workflows/create-blog-post.yml index 8be5662..db205ac 100644 --- a/.github/workflows/create-blog-post.yml +++ b/.github/workflows/create-blog-post.yml @@ -1,41 +1,38 @@ name: Generate Blog Post from Issue - on: issues: types: [opened] - jobs: create-post: if: contains(github.event.issue.labels.*.name, 'blog') runs-on: ubuntu-latest - steps: - name: Checkout Code uses: actions/checkout@v4 - + - name: Generate GitHub App Token id: app-token uses: actions/create-github-app-token@v1 with: app-id: ${{ secrets.APP_ID }} private-key: ${{ secrets.APP_PRIVATE_KEY }} - + - name: Parse Issue Form id: parse uses: stefanbuck/github-issue-parser@v3 with: template-path: .github/ISSUE_TEMPLATE/new-blog-post.yml - + - name: Set up Python uses: actions/setup-python@v5 with: python-version: '3.x' - + - name: Install Dependencies run: | python -m pip install --upgrade pip pip install Pillow - + - name: Process Issue Data and Assets (Python Optimizer) id: process env: @@ -90,17 +87,22 @@ jobs: f.write(optimized_data) if os.path.exists(output_path) and os.path.getsize(output_path) > 0: os.remove(input_path) - print(f"[Processed & Purged] {file} -> {output_file_name}") + print(f"[Processed & Purged] {file} -> {output_file_name}") except Exception as e: print(f"Failed to process {file}. Error: {e}") # --- 2. SETUP FORMS & METADATA --- data = json.loads(os.environ["ISSUE_JSON"]) raw_title = os.environ.get("RAW_TITLE", "Untitled Post") + + # Clean title for frontmatter (escapes existing internal quotes) + safe_title = raw_title.replace('"', '\\"') + date_str = datetime.now().strftime("%Y-%m-%d") def to_kebab(text): - if not text: return "" + if not text: + return "" text = text.lower().strip() text = re.sub(r"[^a-z0-9\s-]", "", text) return re.sub(r"[\s-]+", "-", text).strip("-") @@ -108,8 +110,25 @@ jobs: slug = to_kebab(raw_title) blogfilename = f"{date_str}-{slug}" - authors = [to_kebab(a) for a in data.get("authors", [])] - tags = [to_kebab(t["label"]) for t in data.get("tags", []) if t.get("value") == True] + # Fix: Handles author array safely whether strings or parsed objects + raw_authors = data.get("authors", []) + if isinstance(raw_authors, str): + authors = [to_kebab(a.strip()) for a in raw_authors.split(",") if a.strip()] + else: + authors = [to_kebab(str(a)) for a in raw_authors if a] + + # Fix: Safely parses tags list regardless of type to prevent AttributeError + raw_tags = data.get("tags", []) + tags = [] + if isinstance(raw_tags, list): + for t in raw_tags: + if isinstance(t, dict): + if t.get("value") is True: + tags.append(to_kebab(t.get("label", ""))) + else: + tags.append(to_kebab(str(t))) + elif isinstance(raw_tags, str): + tags = [to_kebab(t.strip()) for t in raw_tags.split(",") if t.strip()] static_folder = f"static/img/blog/{blogfilename}" web_prefix = f"/img/blog/{blogfilename}" @@ -120,14 +139,12 @@ jobs: if cover_url and cover_url != "null": tmp_cover_dir = "/tmp/raw_cover" os.makedirs(tmp_cover_dir, exist_ok=True) - clean_url = cover_url.split("?") _, ext = os.path.splitext(clean_url[0]) - if not ext: ext = ".jpg" - + if not ext: + ext = ".jpg" raw_cover_path = os.path.join(tmp_cover_dir, f"cover{ext}") urllib.request.urlretrieve(cover_url, raw_cover_path) - optimize_convert_and_hash_images(tmp_cover_dir, static_folder, keep_original_names=True) cover_line = f"![Cover Picture]({web_prefix}/cover.webp)" @@ -138,18 +155,15 @@ jobs: if album_url and album_url != "null": tmp_slides_dir = "/tmp/raw_slides" os.makedirs(tmp_slides_dir, exist_ok=True) - zip_path = "album.zip" urllib.request.urlretrieve(album_url, zip_path) with zipfile.ZipFile(zip_path, "r") as zip_ref: zip_ref.extractall(tmp_slides_dir) os.remove(zip_path) - slides_target_folder = os.path.join(static_folder, "slides") optimize_convert_and_hash_images(tmp_slides_dir, slides_target_folder, keep_original_names=True) - has_album = True - album_line = f"" + album_line = f"" # --- 5. PARSE TEXT CONTENT FROM TEXTAREA --- body_text = data.get("blog-content", "").strip() @@ -157,34 +171,29 @@ jobs: content_markdown = "" if paragraphs: content_markdown += paragraphs[0] + "\n\n{/* truncate */}" - if len(paragraphs) > 1: - content_markdown += "\n\n" + "\n\n".join(paragraphs[1:]) + if len(paragraphs) > 1: + content_markdown += "\n\n" + "\n\n".join(paragraphs[1:]) # --- 6. COMPILE FILE VIA CLEAN SEQUENTIAL LIST AND \n JOINS --- - authors_str = ", ".join(authors) - tags_str = ", ".join(tags) - + authors_str = ", ".join([f'"{a}"' for a in authors]) + tags_str = ", ".join([f'"{t}"' for a in tags]) lines = [ "---", - f"title: {raw_title}", + f'title: "{safe_title}"', # Wraps the title safely inside quotations f"authors: [{authors_str}]", f"tags: [{tags_str}]", "---", "" ] - if has_album: lines.append("import PhotoAlbumGallery from '@site/src/components/PhotoAlbumGallery';\n") if cover_line: lines.append(cover_line + "\n") - lines.append(content_markdown) - if has_album and album_line: lines.append("\n" + album_line) file_content = "\n".join(lines).strip() + "\n" - os.makedirs("blog", exist_ok=True) with open(f"blog/{blogfilename}.md", "w", encoding="utf-8") as f: f.write(file_content) @@ -202,7 +211,6 @@ jobs: title: "feat(blog): ${{ github.event.issue.title }}" body: | This Pull Request was automatically generated from Issue #${{ github.event.issue.number }}. - Closes #${{ github.event.issue.number }} labels: | blog @@ -211,11 +219,10 @@ jobs: - name: Comment on Issue with PR Link if: steps.cpr.outputs.pull-request-number != '' uses: peter-evans/create-or-update-comment@v4 - with: + with: token: ${{ steps.app-token.outputs.token }} issue-number: ${{ github.event.issue.number }} body: | - 🎉 **Success!** A staging branch has been created. + 🎉 Success! A staging branch has been created. - Your blog post is ready for review here: - 👉 ${{ steps.cpr.outputs.pull-request-url }} + Your blog post is ready for review here:👉 ${{ steps.cpr.outputs.pull-request-url }} \ No newline at end of file From 6546f7697c2160a2d871fec62b01b4e6b2c32d8a Mon Sep 17 00:00:00 2001 From: shoverbj Date: Fri, 12 Jun 2026 08:40:43 -0400 Subject: [PATCH 14/50] Updating workflows --- .github/ISSUE_TEMPLATE/new-blog-author.yml | 18 ++- .github/ISSUE_TEMPLATE/new-blog-post.yml | 65 +++++++-- .github/workflows/create-blog-post.yml | 160 ++++++++++++++------- 3 files changed, 178 insertions(+), 65 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/new-blog-author.yml b/.github/ISSUE_TEMPLATE/new-blog-author.yml index a731954..6f924ae 100644 --- a/.github/ISSUE_TEMPLATE/new-blog-author.yml +++ b/.github/ISSUE_TEMPLATE/new-blog-author.yml @@ -1,15 +1,23 @@ name: New Blog Author description: "Add a new author to the blog authors list" -title: "Add New Author" +title: "[New Author]" labels: ["new-author"] body: + - type: markdown + attributes: + value: | + **The title above is just for internal use. Please don't change it** - type: input id: name attributes: label: Author's Name - description: "Name shown in blog posts for the author (NOTE: If you are a - Scout this should be your first name followed by a last initial, no last - names please unless you are an adult and want your last name shown)" + description: | + Name shown in blog posts for the author + + NOTE: If you are a Scout this should be your first name followed by + a last initial, **NO LAST NAMES PLEASE** + + If you are an adult and want your last name shown, feel free to include it" placeholder: "Scout A" validations: required: true @@ -17,7 +25,7 @@ body: id: title attributes: label: Title - description: "If you are a Scout, list your position(s) and unit." + description: "List your position(s) and unit." placeholder: "Position, Troop ###" validations: required: true diff --git a/.github/ISSUE_TEMPLATE/new-blog-post.yml b/.github/ISSUE_TEMPLATE/new-blog-post.yml index bcb3b04..90918e9 100644 --- a/.github/ISSUE_TEMPLATE/new-blog-post.yml +++ b/.github/ISSUE_TEMPLATE/new-blog-post.yml @@ -1,23 +1,46 @@ name: New Blog Post description: "Add a new post to the blog" labels: ["blog"] -title: "Replace this with your blog title" +title: "[New Blog Post]" body: - type: markdown attributes: value: | - **The title above will be the title of your blog post** + **The title above is just for internal use. Please don't change it** - Your title should highlight what the post is about (campout/event/etc.) + # Creating new blog post Fill out this form and click "Create" below to submit a new blog post. After submitting your post, it will be reviewed by the webmaster before being posted to the website. + - type: input + id: title + attributes: + label: "Blog Title" + description: | + Title for your post + + This should include the campout/adventure/event if thats what this post + is about! + placeholder: "Our Recent Adventures" + validations: + required: true + - type: input + id: date + attributes: + label: "Post Date" + description: | + Date for your blog post + + If this is left empty, today's date will be used. + placeholder: YYYY-MM-DD + validations: + required: false - type: dropdown id: authors attributes: - label: "Select authors" - description: "Use new author issue form if the author isn't listed here" + label: "Authors" + description: "Select author(s) of this post" multiple: true options: # AUTHOR_START @@ -26,11 +49,22 @@ body: # AUTHOR_END validations: required: true - - type: checkboxes + - type: markdown + attributes: + value: | + If your name is not in the list of authors, please submit a request to + add your name to the list of authors here: + + [New Author Request](https://github.com/scouting331/scoutSite/issues/new?template=new-blog-author.yml) + - type: dropdown id: tags attributes: - label: Unit - description: "Select the unit(s) to associate this post with" + label: "Unit" + description: | + Select the unit(s) to associate this post with. + + Multiple units may be selected. + multiple: true options: - label: Troop 303 - label: Troop 331 @@ -50,7 +84,12 @@ body: id: photo-album attributes: label: "Photo Album" - description: "Compress folder of photos as a zip file before uploading. If the file is too big (>25 Mb), the photo album will need to be added manually (see Mr. Shover)" + description: | + If you want to include a photo album upload it here. + + Compress the folder of photos as a zip file before uploading. + + NOTE: If the resulting file is too big (>25 Mb), the photo album will need to be added manually (see webmaster) validations: required: false accept: ".zip" @@ -58,8 +97,12 @@ body: id: blog-content attributes: label: "Blog Text" - description: "Write your blog post here!" + description: | + Write your blog post here! placeholder: "Blog text here..." - render: text validations: required: true + - type: markdown + attributes: + value: | + **Please don't edit any of the other options!** diff --git a/.github/workflows/create-blog-post.yml b/.github/workflows/create-blog-post.yml index db205ac..58a6836 100644 --- a/.github/workflows/create-blog-post.yml +++ b/.github/workflows/create-blog-post.yml @@ -31,13 +31,12 @@ jobs: - name: Install Dependencies run: | python -m pip install --upgrade pip - pip install Pillow + pip install Pillow python-dateutil - name: Process Issue Data and Assets (Python Optimizer) id: process env: ISSUE_JSON: ${{ steps.parse.outputs.jsonString }} - RAW_TITLE: ${{ github.event.issue.title }} shell: python run: | import os @@ -48,14 +47,17 @@ jobs: import zipfile from io import BytesIO from datetime import datetime + from dateutil import parser from PIL import Image, ImageOps # --- 1. INTEGRATED OPTIMIZER ENGINE CODE --- def optimize_convert_and_hash_images(input_dir, output_dir, max_size=(1920, 1080), quality=80, keep_original_names=False): if not os.path.exists(input_dir): - return + return {} os.makedirs(output_dir, exist_ok=True) - valid_extensions = (".jpg", ".jpeg", ".png") + valid_extensions = (".jpg", ".jpeg", ".png", ".webp") + url_to_new_path_map = {} + for root, _, files in os.walk(input_dir): for file in files: if file.lower().endswith(valid_extensions): @@ -72,6 +74,7 @@ jobs: buffer = BytesIO() img.save(buffer, format="WEBP", quality=quality) optimized_data = buffer.getvalue() + if keep_original_names: base_name, _ = os.path.splitext(file) output_file_name = f"{base_name}.webp" @@ -79,26 +82,42 @@ jobs: hasher = hashlib.md5(optimized_data) content_hash = hasher.hexdigest() output_file_name = f"img_{content_hash}.webp" + output_path = os.path.join(target_folder, output_file_name) - if os.path.exists(output_path): - os.remove(input_path) - continue - with open(output_path, "wb") as f: - f.write(optimized_data) - if os.path.exists(output_path) and os.path.getsize(output_path) > 0: + + if not os.path.exists(output_path): + with open(output_path, "wb") as f: + f.write(optimized_data) + + if os.path.exists(input_path): os.remove(input_path) + + with open(os.path.join(root, f"{file}.ref"), "r") as ref_f: + orig_url = ref_f.read().strip() + url_to_new_path_map[orig_url] = output_file_name + print(f"[Processed & Purged] {file} -> {output_file_name}") except Exception as e: print(f"Failed to process {file}. Error: {e}") + return url_to_new_path_map # --- 2. SETUP FORMS & METADATA --- data = json.loads(os.environ["ISSUE_JSON"]) - raw_title = os.environ.get("RAW_TITLE", "Untitled Post") - # Clean title for frontmatter (escapes existing internal quotes) + raw_title = data.get("title", "Untitled Post").strip() safe_title = raw_title.replace('"', '\\"') - date_str = datetime.now().strftime("%Y-%m-%d") + form_date = data.get("date", "").strip() + current_time = datetime.now() + date_str = current_time.strftime("%Y-%m-%d") + + if form_date and form_date != "null" and form_date != "_No response_": + try: + year_anchor = datetime(current_time.year, 1, 1) + parsed_date = parser.parse(form_date, default=year_anchor, fuzzy=True) + date_str = parsed_date.strftime("%Y-%m-%d") + except Exception as e: + print(f"[Date Parsing Alert] Fallback used. Error: {e}") def to_kebab(text): if not text: @@ -110,25 +129,17 @@ jobs: slug = to_kebab(raw_title) blogfilename = f"{date_str}-{slug}" - # Fix: Handles author array safely whether strings or parsed objects - raw_authors = data.get("authors", []) + raw_authors = data.get("authors", "") if isinstance(raw_authors, str): - authors = [to_kebab(a.strip()) for a in raw_authors.split(",") if a.strip()] + authors = [to_kebab(a) for a in raw_authors.split(",") if a.strip()] + else: + authors = [to_kebab(str(raw_authors))] + + raw_tags = data.get("tags", "") + if isinstance(raw_tags, str): + tags = [to_kebab(t) for t in raw_tags.split(",") if t.strip()] else: - authors = [to_kebab(str(a)) for a in raw_authors if a] - - # Fix: Safely parses tags list regardless of type to prevent AttributeError - raw_tags = data.get("tags", []) - tags = [] - if isinstance(raw_tags, list): - for t in raw_tags: - if isinstance(t, dict): - if t.get("value") is True: - tags.append(to_kebab(t.get("label", ""))) - else: - tags.append(to_kebab(str(t))) - elif isinstance(raw_tags, str): - tags = [to_kebab(t.strip()) for t in raw_tags.split(",") if t.strip()] + tags = [to_kebab(str(raw_tags))] static_folder = f"static/img/blog/{blogfilename}" web_prefix = f"/img/blog/{blogfilename}" @@ -136,7 +147,7 @@ jobs: # --- 3. FETCH AND RUN OPTIMIZER ON COVER PHOTO --- cover_url = data.get("cover-photo") cover_line = "" - if cover_url and cover_url != "null": + if cover_url and cover_url != "null" and cover_url != "_No response_": tmp_cover_dir = "/tmp/raw_cover" os.makedirs(tmp_cover_dir, exist_ok=True) clean_url = cover_url.split("?") @@ -144,29 +155,79 @@ jobs: if not ext: ext = ".jpg" raw_cover_path = os.path.join(tmp_cover_dir, f"cover{ext}") - urllib.request.urlretrieve(cover_url, raw_cover_path) - optimize_convert_and_hash_images(tmp_cover_dir, static_folder, keep_original_names=True) - cover_line = f"![Cover Picture]({web_prefix}/cover.webp)" + try: + urllib.request.urlretrieve(cover_url, raw_cover_path) + with open(f"{raw_cover_path}.ref", "w") as ref_f: + ref_f.write("cover") + optimize_convert_and_hash_images(tmp_cover_dir, static_folder, keep_original_names=True) + cover_line = f"![Cover Picture]({web_prefix}/cover.webp)" + except Exception as e: + print(f"Skipping cover photo download. Error: {e}") # --- 4. FETCH AND RUN OPTIMIZER ON ZIP ALBUM SLIDES --- album_url = data.get("photo-album") has_album = False album_line = "" - if album_url and album_url != "null": + if album_url and album_url != "null" and album_url != "_No response_": tmp_slides_dir = "/tmp/raw_slides" os.makedirs(tmp_slides_dir, exist_ok=True) zip_path = "album.zip" - urllib.request.urlretrieve(album_url, zip_path) - with zipfile.ZipFile(zip_path, "r") as zip_ref: - zip_ref.extractall(tmp_slides_dir) - os.remove(zip_path) - slides_target_folder = os.path.join(static_folder, "slides") - optimize_convert_and_hash_images(tmp_slides_dir, slides_target_folder, keep_original_names=True) - has_album = True - album_line = f"" - - # --- 5. PARSE TEXT CONTENT FROM TEXTAREA --- + try: + urllib.request.urlretrieve(album_url, zip_path) + with zipfile.ZipFile(zip_path, "r") as zip_ref: + zip_ref.extractall(tmp_slides_dir) + os.remove(zip_path) + + for root, _, files in os.walk(tmp_slides_dir): + for file in files: + if file.lower().endswith((".jpg", ".jpeg", ".png", ".webp")): + with open(os.path.join(root, f"{file}.ref"), "w") as ref_f: + ref_f.write("album") + + slides_target_folder = os.path.join(static_folder, "slides") + optimize_convert_and_hash_images(tmp_slides_dir, slides_target_folder, keep_original_names=True) + has_album = True + album_line = f"" + except Exception as e: + print(f"Skipping album download. Error: {e}") + + # --- 5. PARSE & PROCESS INLINE CONTENT PHOTOS --- body_text = data.get("blog-content", "").strip() + + inline_markdown_urls = re.findall(r'!\[.*?\]\((.*?)\)', body_text) + inline_html_urls = re.findall(r']*src=["\'](.*?)["\']', body_text) + all_inline_urls = list(set(inline_markdown_urls + inline_html_urls)) + + if all_inline_urls: + tmp_inline_dir = "/tmp/raw_inline" + os.makedirs(tmp_inline_dir, exist_ok=True) + print(f"[Inline Images Detected] Found {len(all_inline_urls)} embedded image assets.") + + for idx, img_url in enumerate(all_inline_urls): + if not img_url.startswith("http") or img_url == "null": + continue + try: + clean_img_url = img_url.split("?") + _, ext = os.path.splitext(clean_img_url[0]) + if not ext: + ext = ".jpg" + + filename = f"inline_img_{idx}{ext}" + full_tmp_path = os.path.join(tmp_inline_dir, filename) + + urllib.request.urlretrieve(img_url, full_tmp_path) + with open(f"{full_tmp_path}.ref", "w") as ref_f: + ref_f.write(img_url) + except Exception as e: + print(f"Failed to pull inline asset reference: {img_url}. Error: {e}") + + url_to_filename_map = optimize_convert_and_hash_images(tmp_inline_dir, static_folder, keep_original_names=False) + + for original_url, new_filename in url_to_filename_map.items(): + optimized_web_path = f"{web_prefix}/{new_filename}" + body_text = body_text.replace(original_url, optimized_web_path) + print(f"[Rewriting Asset Path] {original_url} -> {optimized_web_path}") + paragraphs = [p.strip() for p in body_text.split("\n\n") if p.strip()] content_markdown = "" if paragraphs: @@ -179,7 +240,8 @@ jobs: tags_str = ", ".join([f'"{t}"' for a in tags]) lines = [ "---", - f'title: "{safe_title}"', # Wraps the title safely inside quotations + f'title: "{safe_title}"', + f"date: {date_str}", f"authors: [{authors_str}]", f"tags: [{tags_str}]", "---", @@ -195,7 +257,8 @@ jobs: file_content = "\n".join(lines).strip() + "\n" os.makedirs("blog", exist_ok=True) - with open(f"blog/{blogfilename}.md", "w", encoding="utf-8") as f: + + with open(f"blog/{blogfilename}.mdx", "w", encoding="utf-8") as f: f.write(file_content) with open(os.environ["GITHUB_ENV"], "a") as env_file: @@ -215,11 +278,10 @@ jobs: labels: | blog ready-to-review - - name: Comment on Issue with PR Link if: steps.cpr.outputs.pull-request-number != '' uses: peter-evans/create-or-update-comment@v4 - with: + with: token: ${{ steps.app-token.outputs.token }} issue-number: ${{ github.event.issue.number }} body: | From 5fbe13603d473f083cab48e1ad4e1bc3dbbc9669 Mon Sep 17 00:00:00 2001 From: shoverbj Date: Fri, 12 Jun 2026 08:44:04 -0400 Subject: [PATCH 15/50] fix unit options --- .github/ISSUE_TEMPLATE/new-blog-post.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/new-blog-post.yml b/.github/ISSUE_TEMPLATE/new-blog-post.yml index 90918e9..a2192f4 100644 --- a/.github/ISSUE_TEMPLATE/new-blog-post.yml +++ b/.github/ISSUE_TEMPLATE/new-blog-post.yml @@ -66,10 +66,10 @@ body: Multiple units may be selected. multiple: true options: - - label: Troop 303 - - label: Troop 331 - - label: Crew 303 - - label: Pack 303 + - Troop 303 + - Troop 331 + - Crew 303 + - Pack 303 validations: required: true - type: upload From 8ebc479233953df12327605780cd812c08f641fd Mon Sep 17 00:00:00 2001 From: shoverbj Date: Fri, 12 Jun 2026 08:53:57 -0400 Subject: [PATCH 16/50] fix workflow --- .github/workflows/create-blog-post.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/create-blog-post.yml b/.github/workflows/create-blog-post.yml index 58a6836..6cce2e7 100644 --- a/.github/workflows/create-blog-post.yml +++ b/.github/workflows/create-blog-post.yml @@ -135,6 +135,7 @@ jobs: else: authors = [to_kebab(str(raw_authors))] + # This section handles parsing whether options are flat strings or structured maps raw_tags = data.get("tags", "") if isinstance(raw_tags, str): tags = [to_kebab(t) for t in raw_tags.split(",") if t.strip()] From 6b7fe69f0a6b17f65ad53d95cfa4b5252252f2a6 Mon Sep 17 00:00:00 2001 From: shoverbj Date: Fri, 12 Jun 2026 08:55:44 -0400 Subject: [PATCH 17/50] Fixed list comprehension variable --- .github/workflows/create-blog-post.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/create-blog-post.yml b/.github/workflows/create-blog-post.yml index 6cce2e7..d3ed3ad 100644 --- a/.github/workflows/create-blog-post.yml +++ b/.github/workflows/create-blog-post.yml @@ -238,7 +238,7 @@ jobs: # --- 6. COMPILE FILE VIA CLEAN SEQUENTIAL LIST AND \n JOINS --- authors_str = ", ".join([f'"{a}"' for a in authors]) - tags_str = ", ".join([f'"{t}"' for a in tags]) + tags_str = ", ".join([f'"{t}"' for t in tags]) lines = [ "---", f'title: "{safe_title}"', From fb82c73e18db89dd1f6cc2178f99db8074f53ebf Mon Sep 17 00:00:00 2001 From: shoverbj Date: Fri, 12 Jun 2026 09:20:31 -0400 Subject: [PATCH 18/50] testing file uploads --- .github/ISSUE_TEMPLATE/new-blog-author.yml | 1 + .github/ISSUE_TEMPLATE/new-blog-post.yml | 4 +++- blog/2026-04-27-western-camporee.mdx | 1 + blog/2026-04-28-railroading.mdx | 1 + blog/2026-06-03-spring-coh.mdx | 1 + blog/2026-06-09-scoutmasters-blog.mdx | 1 + blog/2026-06-10-scoutmasters-blog.mdx | 1 + blog/authors.yml | 2 +- 8 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/new-blog-author.yml b/.github/ISSUE_TEMPLATE/new-blog-author.yml index 6f924ae..ec90fa2 100644 --- a/.github/ISSUE_TEMPLATE/new-blog-author.yml +++ b/.github/ISSUE_TEMPLATE/new-blog-author.yml @@ -39,4 +39,5 @@ body: validations: required: false accept: ".png, .jpg, .jpeg, .gif, .svg, .webp" + multiple: false diff --git a/.github/ISSUE_TEMPLATE/new-blog-post.yml b/.github/ISSUE_TEMPLATE/new-blog-post.yml index a2192f4..ae4d3e3 100644 --- a/.github/ISSUE_TEMPLATE/new-blog-post.yml +++ b/.github/ISSUE_TEMPLATE/new-blog-post.yml @@ -44,7 +44,7 @@ body: multiple: true options: # AUTHOR_START - - Christopher Koczan + - Chris Koczan - Benjamin Shover # AUTHOR_END validations: @@ -80,6 +80,7 @@ body: validations: required: false accept: ".png, .jpg, .jpeg, .gif, .svg, .webp" + multiple: false - type: upload id: photo-album attributes: @@ -93,6 +94,7 @@ body: validations: required: false accept: ".zip" + multiple: true - type: textarea id: blog-content attributes: diff --git a/blog/2026-04-27-western-camporee.mdx b/blog/2026-04-27-western-camporee.mdx index 8881855..6b29d27 100644 --- a/blog/2026-04-27-western-camporee.mdx +++ b/blog/2026-04-27-western-camporee.mdx @@ -1,5 +1,6 @@ --- title: Western Division Spring Camporee +date: 2026-04-27 authors: [benjamin-shover] tags: [troop-303] --- diff --git a/blog/2026-04-28-railroading.mdx b/blog/2026-04-28-railroading.mdx index 66b94ea..396af9d 100644 --- a/blog/2026-04-28-railroading.mdx +++ b/blog/2026-04-28-railroading.mdx @@ -1,5 +1,6 @@ --- title: Railroading Camporee +date: 2026-04-28 authors: [benjamin-shover] tags: [troop-331] --- diff --git a/blog/2026-06-03-spring-coh.mdx b/blog/2026-06-03-spring-coh.mdx index 8ede0a3..998bf60 100644 --- a/blog/2026-06-03-spring-coh.mdx +++ b/blog/2026-06-03-spring-coh.mdx @@ -1,5 +1,6 @@ --- title: Spring Court of Honor +date: 2026-06-03 authors: [benjamin-shover] tags: [troop-303, troop-331] --- diff --git a/blog/2026-06-09-scoutmasters-blog.mdx b/blog/2026-06-09-scoutmasters-blog.mdx index 4938032..d39a404 100644 --- a/blog/2026-06-09-scoutmasters-blog.mdx +++ b/blog/2026-06-09-scoutmasters-blog.mdx @@ -1,5 +1,6 @@ --- title: "Day 1: A Scoutmaster's Blog" +date: 2026-06-09 authors: [chris-koczan] tags: [troop-303] --- diff --git a/blog/2026-06-10-scoutmasters-blog.mdx b/blog/2026-06-10-scoutmasters-blog.mdx index a0fc8af..fa3ac6b 100644 --- a/blog/2026-06-10-scoutmasters-blog.mdx +++ b/blog/2026-06-10-scoutmasters-blog.mdx @@ -1,5 +1,6 @@ --- title: "Day 2: A Scoutmaster's Blog" +date: 2026-06-10 authors: [chris-koczan] tags: [troop-303] --- diff --git a/blog/authors.yml b/blog/authors.yml index a5ed5c3..73c6533 100644 --- a/blog/authors.yml +++ b/blog/authors.yml @@ -23,7 +23,7 @@ benjamin-shover: page: true chris-koczan: - name: Christopher Koczan + name: Chris Koczan title: Scoutmaster, Troop 303 image_url: /img/blog/authors/chris-koczan.jpg page: true \ No newline at end of file From 360334081815ed79979185ef30e4bacd7080bc6a Mon Sep 17 00:00:00 2001 From: shoverbj Date: Fri, 12 Jun 2026 09:33:20 -0400 Subject: [PATCH 19/50] process author avatar correctly --- .github/ISSUE_TEMPLATE/new-blog-author.yml | 1 - .github/ISSUE_TEMPLATE/new-blog-post.yml | 2 - .github/workflows/process-new-authors.yml | 106 ++++++++++++--------- 3 files changed, 59 insertions(+), 50 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/new-blog-author.yml b/.github/ISSUE_TEMPLATE/new-blog-author.yml index ec90fa2..6f924ae 100644 --- a/.github/ISSUE_TEMPLATE/new-blog-author.yml +++ b/.github/ISSUE_TEMPLATE/new-blog-author.yml @@ -39,5 +39,4 @@ body: validations: required: false accept: ".png, .jpg, .jpeg, .gif, .svg, .webp" - multiple: false diff --git a/.github/ISSUE_TEMPLATE/new-blog-post.yml b/.github/ISSUE_TEMPLATE/new-blog-post.yml index ae4d3e3..f4e8532 100644 --- a/.github/ISSUE_TEMPLATE/new-blog-post.yml +++ b/.github/ISSUE_TEMPLATE/new-blog-post.yml @@ -80,7 +80,6 @@ body: validations: required: false accept: ".png, .jpg, .jpeg, .gif, .svg, .webp" - multiple: false - type: upload id: photo-album attributes: @@ -94,7 +93,6 @@ body: validations: required: false accept: ".zip" - multiple: true - type: textarea id: blog-content attributes: diff --git a/.github/workflows/process-new-authors.yml b/.github/workflows/process-new-authors.yml index 85dea89..db742b7 100644 --- a/.github/workflows/process-new-authors.yml +++ b/.github/workflows/process-new-authors.yml @@ -3,32 +3,37 @@ name: Process New Author Request on: issues: types: [opened] - jobs: add-author-and-pr: - # Only run if the issue has your specific 'new-author' label if: contains(github.event.issue.labels.*.name, 'new-author') runs-on: ubuntu-latest steps: - name: Checkout Repo uses: actions/checkout@v4 - # 1. Generate bot token from your GitHub App - name: Generate Token id: app-token uses: actions/create-github-app-token@v1 with: app-id: ${{ secrets.APP_ID }} private-key: ${{ secrets.APP_PRIVATE_KEY }} - - # 2. Parse the issue template form data into JSON + - name: Parse Issue Form id: parse uses: stefanbuck/github-issue-parser@v3 with: template-path: .github/ISSUE_TEMPLATE/new-blog-author.yml - # 3. Process the data and append it to blog/authors.yml + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.x' + + - name: Install Dependencies + run: | + python -m pip install --upgrade pip + pip install Pillow PyYAML python-dateutil + - name: Process Author and Update Files id: process_files shell: python @@ -40,20 +45,19 @@ jobs: import re import os import sys + import urllib.request + from PIL import Image, ImageOps - # Define file paths AUTHORS_FILE = 'blog/authors.yml' - TEMPLATE_FILE = '.github/ISSUE_TEMPLATE/new-blog-author.yml' + TEMPLATE_FILE = '.github/ISSUE_TEMPLATE/new-blog-post.yml' + AUTHORS_IMG_DIR = 'static/img/blog/authors' - # Read parsed issue data issue_json = os.environ.get("ISSUE_DATA", "{}") data = json.loads(issue_json) - author_name = data.get("name", "").strip() author_title = data.get("title", "").strip() raw_image_url = data.get("image_url", "").strip() - - # Extract image URL + image_url = "" url_match = re.search(r'\((https://[^\)]+)\)', raw_image_url) if url_match: @@ -63,78 +67,96 @@ jobs: print("Missing required fields. Exiting.") sys.exit(1) - # Read existing authors if os.path.exists(AUTHORS_FILE): with open(AUTHORS_FILE, 'r') as f: authors_data = yaml.safe_load(f) or {} else: authors_data = {} - # Duplicate check for existing_key, existing_details in authors_data.items(): if isinstance(existing_details, dict) and existing_details.get('name', '').lower() == author_name.lower(): print(f"::error::The author name '{author_name}' already exists.") sys.exit(1) - # Generate unique key slug slug = author_name.lower() slug = re.sub(r'[^a-z0-9\s-]', '', slug) slug = re.sub(r'[\s-]+', '-', slug).strip('-') - + final_slug = slug counter = 1 while final_slug in authors_data: final_slug = f"{slug}-{counter}" counter += 1 - # 1. Update blog/authors.yml + final_image_path = "" + if image_url: + os.makedirs(AUTHORS_IMG_DIR, exist_ok=True) + clean_url = image_url.split("?")[0] + _, ext = os.path.splitext(clean_url) + if not ext: + ext = ".jpg" + + tmp_avatar_path = f"/tmp/raw_avatar{ext}" + try: + urllib.request.urlretrieve(image_url, tmp_avatar_path) + + target_file_name = f"{final_slug}.webp" + target_full_path = os.path.join(AUTHORS_IMG_DIR, target_file_name) + + with Image.open(tmp_avatar_path) as img: + img = ImageOps.exif_transpose(img) + if img.mode in ("P", "CMYK"): + img = img.convert("RGBA") + img.thumbnail((500, 500), Image.Resampling.LANCZOS) + img.save(target_full_path, format="WEBP", quality=85) + + final_image_path = f"/img/blog/authors/{target_file_name}" + print(f"Successfully processed and saved avatar to {target_full_path}") + + if os.path.exists(tmp_avatar_path): + os.remove(tmp_avatar_path) + except Exception as e: + print(f"Warning: Failed to download or process avatar image. Error: {e}") + new_author_entry = { "name": author_name, "title": author_title, "page": True } - if image_url: - new_author_entry["image_url"] = image_url - + if final_image_path: + new_author_entry["image_url"] = final_image_path + authors_data[final_slug] = new_author_entry with open(AUTHORS_FILE, 'w') as f: yaml.dump(authors_data, f, sort_keys=False, allow_unicode=True) - print(f"Successfully updated {AUTHORS_FILE}") - # 2. Extract all names (including the new one) and update the dropdown template all_names = [] for key, details in authors_data.items(): if isinstance(details, dict) and 'name' in details: all_names.append(details['name']) - all_names.sort() - # Format list for the template dropdown indents (8 spaces padding) yaml_lines = [f" - {name}" for name in all_names] replacement_string = "\n".join(yaml_lines) - # Read and update template file if os.path.exists(TEMPLATE_FILE): with open(TEMPLATE_FILE, 'r') as f: template_content = f.read() - pattern = r'(# AUTHOR_START\n)(.*?)(\n\s*# AUTHOR_END)' updated_content = re.sub( - pattern, - f"\\1{replacement_string}\\3", - template_content, + pattern, + f"\\1{replacement_string}\\3", + template_content, flags=re.DOTALL ) - with open(TEMPLATE_FILE, 'w') as f: f.write(updated_content) print(f"Successfully updated dropdown in {TEMPLATE_FILE}") else: print(f"Warning: Template file {TEMPLATE_FILE} not found. Skipping dropdown injection.") - - # 4. Open the Pull Request on behalf of the bot app + - name: Create Pull Request if: success() uses: peter-evans/create-pull-request@v6 @@ -143,21 +165,18 @@ jobs: commit-message: "feat: add new blog author via Issue #${{ github.event.issue.number }}" title: "feat: add new author from Issue #${{ github.event.issue.number }}" body: | - This PR automatically handles two tasks: - 1. Adds the new author to `blog/authors.yml`. - 2. Re-generates and sorts the author dropdown options inside the issue templates. - + This PR automatically handles three tasks: + 1. Downloads, optimizes, and names the avatar image matching the unique author slug. + 2. Adds the new author data mapping properties into `blog/authors.yml`. + 3. Re-generates and sorts the author dropdown options inside the issue templates. Closes #${{ github.event.issue.number }}. - CODEOWNERS have been automatically assigned to review. - branch: "automation/add-author-issue-${{ github.event.issue.number }}" delete-branch: true labels: | blog automation - # 5. Comment on success - name: Comment on Success if: success() uses: peter-evans/create-or-update-comment@v4 @@ -165,11 +184,8 @@ jobs: token: ${{ steps.app-token.outputs.token }} issue-number: ${{ github.event.issue.number }} body: | - Hi there! An automated Pull Request has been generated to add you to the blog authors file and update our submission dropdown tools. - - The project's CODEOWNERS have been notified to review and merge the changes. Once merged, your author profile and dropdown options will be active! + Hi there! An automated Pull Request has been generated to add you to the blog authors file and update our submission dropdown tools. The project's CODEOWNERS have been notified to review and merge the changes. Once merged, your author profile and dropdown options will be active! - # 6. Handle failure / duplicates (Only runs if the Python script failed) - name: Handle Duplicate / Failure if: failure() uses: actions/github-script@v7 @@ -177,16 +193,12 @@ jobs: github-token: ${{ steps.app-token.outputs.token }} script: | const issueNumber = context.issue.number; - - // 1. Leave an error comment await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: issueNumber, body: "🛑 **Registration Error:** It looks like an author profile with this name already exists in `blog/authors.yml`. Duplicate entries are not allowed. This issue will now be closed automatically." }); - - // 2. Close the issue automatically await github.rest.issues.update({ owner: context.repo.owner, repo: context.repo.repo, From 4276ed9c599beb70c007244f974a66abe5fcb62f Mon Sep 17 00:00:00 2001 From: shoverbj Date: Fri, 12 Jun 2026 09:48:03 -0400 Subject: [PATCH 20/50] fixes to authors appending --- .github/workflows/process-new-authors.yml | 59 ++++++++++++----------- 1 file changed, 32 insertions(+), 27 deletions(-) diff --git a/.github/workflows/process-new-authors.yml b/.github/workflows/process-new-authors.yml index db742b7..6bf41f4 100644 --- a/.github/workflows/process-new-authors.yml +++ b/.github/workflows/process-new-authors.yml @@ -67,14 +67,13 @@ jobs: print("Missing required fields. Exiting.") sys.exit(1) + # Read existing content to check for duplicates without rewriting via safe_load if os.path.exists(AUTHORS_FILE): - with open(AUTHORS_FILE, 'r') as f: - authors_data = yaml.safe_load(f) or {} - else: - authors_data = {} - - for existing_key, existing_details in authors_data.items(): - if isinstance(existing_details, dict) and existing_details.get('name', '').lower() == author_name.lower(): + with open(AUTHORS_FILE, 'r', encoding='utf-8') as f: + raw_content = f.read() + + # Simple case-insensitive duplicate name check + if f"name: {author_name}" in raw_content or f'name: "{author_name}"' in raw_content: print(f"::error::The author name '{author_name}' already exists.") sys.exit(1) @@ -84,15 +83,17 @@ jobs: final_slug = slug counter = 1 - while final_slug in authors_data: + + # Scan file text to ensure slug uniqueness + while f"{final_slug}:" in raw_content: final_slug = f"{slug}-{counter}" counter += 1 final_image_path = "" if image_url: os.makedirs(AUTHORS_IMG_DIR, exist_ok=True) - clean_url = image_url.split("?")[0] - _, ext = os.path.splitext(clean_url) + clean_url = image_url.split("?") + _, ext = os.path.splitext(clean_url[0]) if not ext: ext = ".jpg" @@ -118,24 +119,29 @@ jobs: except Exception as e: print(f"Warning: Failed to download or process avatar image. Error: {e}") - new_author_entry = { - "name": author_name, - "title": author_title, - "page": True - } + # Prepare the clean textual representation block manually to preserve docstrings + entry_lines = [ + f"{final_slug}:", + f" name: {author_name}", + f" title: {author_title}", + " page: true" + ] if final_image_path: - new_author_entry["image_url"] = final_image_path - - authors_data[final_slug] = new_author_entry + entry_lines.append(f" image_url: {final_image_path}") + + raw_append_block = "\n" + "\n".join(entry_lines) + "\n" - with open(AUTHORS_FILE, 'w') as f: - yaml.dump(authors_data, f, sort_keys=False, allow_unicode=True) - print(f"Successfully updated {AUTHORS_FILE}") + # Append directly to the bottom of the filesystem document + with open(AUTHORS_FILE, 'a', encoding='utf-8') as f: + f.write(raw_append_block) + print(f"Successfully appended new profile block to {AUTHORS_FILE}") - all_names = [] - for key, details in authors_data.items(): - if isinstance(details, dict) and 'name' in details: - all_names.append(details['name']) + # --- 2. REGEX EXTRACT NAMES TO BUILD TEMPLATE DROPDOWN --- + with open(AUTHORS_FILE, 'r', encoding='utf-8') as f: + updated_raw_content = f.read() + + all_names = re.findall(r'^\s*name:\s*["\']?(.*?)["\']?\s*$', updated_raw_content, re.MULTILINE) + all_names = [n.strip() for n in all_names if n.strip()] all_names.sort() yaml_lines = [f" - {name}" for name in all_names] @@ -174,8 +180,7 @@ jobs: branch: "automation/add-author-issue-${{ github.event.issue.number }}" delete-branch: true labels: | - blog - automation + new-author - name: Comment on Success if: success() From 6955cdb6d47817f37959f1d9a8190410c5ecf4a4 Mon Sep 17 00:00:00 2001 From: shoverbj Date: Fri, 12 Jun 2026 10:23:39 -0400 Subject: [PATCH 21/50] added default author image --- docusaurus.config.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/docusaurus.config.js b/docusaurus.config.js index a78f778..af15a5d 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -106,7 +106,21 @@ const config = { onInlineTags: "warn", onInlineAuthors: "warn", onUntruncatedBlogPosts: "warn", - } + processBlogPosts: async ({ blogPosts }) => { + const DEFAULT_IMAGE = '/img/logos/favicon.png'; + + return blogPosts.map((post) => { + if (post.metadata && post.metadata.authors) { + post.metadata.authors = post.metadata.authors.map((author) => ({ + ...author, + // If imageURL is missing, use the default image + imageURL: author.imageURL || DEFAULT_IMAGE, + })); + } + return post; + }); + }, + }, ], ], From 203e73c3cdd3b317b4f34a1fd2e98aee89053583 Mon Sep 17 00:00:00 2001 From: shoverbj Date: Fri, 12 Jun 2026 12:26:59 -0400 Subject: [PATCH 22/50] rename templates --- .../ISSUE_TEMPLATE/{new-blog-post.yml => 01-new-blog-post.yml} | 0 .github/ISSUE_TEMPLATE/{new-document.md => 02-new-document.md} | 0 .../{new-blog-author.yml => 03-new-blog-author.yml} | 0 .github/ISSUE_TEMPLATE/{bug_report.md => 04-bug_report.md} | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename .github/ISSUE_TEMPLATE/{new-blog-post.yml => 01-new-blog-post.yml} (100%) rename .github/ISSUE_TEMPLATE/{new-document.md => 02-new-document.md} (100%) rename .github/ISSUE_TEMPLATE/{new-blog-author.yml => 03-new-blog-author.yml} (100%) rename .github/ISSUE_TEMPLATE/{bug_report.md => 04-bug_report.md} (100%) diff --git a/.github/ISSUE_TEMPLATE/new-blog-post.yml b/.github/ISSUE_TEMPLATE/01-new-blog-post.yml similarity index 100% rename from .github/ISSUE_TEMPLATE/new-blog-post.yml rename to .github/ISSUE_TEMPLATE/01-new-blog-post.yml diff --git a/.github/ISSUE_TEMPLATE/new-document.md b/.github/ISSUE_TEMPLATE/02-new-document.md similarity index 100% rename from .github/ISSUE_TEMPLATE/new-document.md rename to .github/ISSUE_TEMPLATE/02-new-document.md diff --git a/.github/ISSUE_TEMPLATE/new-blog-author.yml b/.github/ISSUE_TEMPLATE/03-new-blog-author.yml similarity index 100% rename from .github/ISSUE_TEMPLATE/new-blog-author.yml rename to .github/ISSUE_TEMPLATE/03-new-blog-author.yml diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/04-bug_report.md similarity index 100% rename from .github/ISSUE_TEMPLATE/bug_report.md rename to .github/ISSUE_TEMPLATE/04-bug_report.md From cfb01a07c465e98f37b6741edc5072fac42d7b2f Mon Sep 17 00:00:00 2001 From: shoverbj Date: Fri, 12 Jun 2026 12:27:33 -0400 Subject: [PATCH 23/50] rename created branches --- .github/workflows/create-blog-post.yml | 4 ++-- .github/workflows/process-new-authors.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/create-blog-post.yml b/.github/workflows/create-blog-post.yml index d3ed3ad..d85fe67 100644 --- a/.github/workflows/create-blog-post.yml +++ b/.github/workflows/create-blog-post.yml @@ -271,14 +271,14 @@ jobs: with: token: ${{ steps.app-token.outputs.token }} commit-message: "feat(blog): add new post from issue #${{ github.event.issue.number }}" - branch: "blog/issue-${{ github.event.issue.number }}-${{ env.BLOG_FILENAME }}" + branch: "automation/issue-${{ github.event.issue.number }}-${{ env.BLOG_FILENAME }}" title: "feat(blog): ${{ github.event.issue.title }}" body: | This Pull Request was automatically generated from Issue #${{ github.event.issue.number }}. Closes #${{ github.event.issue.number }} + delete-branch: true labels: | blog - ready-to-review - name: Comment on Issue with PR Link if: steps.cpr.outputs.pull-request-number != '' uses: peter-evans/create-or-update-comment@v4 diff --git a/.github/workflows/process-new-authors.yml b/.github/workflows/process-new-authors.yml index 6bf41f4..2ee8db6 100644 --- a/.github/workflows/process-new-authors.yml +++ b/.github/workflows/process-new-authors.yml @@ -177,7 +177,7 @@ jobs: 3. Re-generates and sorts the author dropdown options inside the issue templates. Closes #${{ github.event.issue.number }}. CODEOWNERS have been automatically assigned to review. - branch: "automation/add-author-issue-${{ github.event.issue.number }}" + branch: "automation/issue-${{ github.event.issue.number }}" delete-branch: true labels: | new-author From a127e1b183e3beafd3a4ef0d42ffe3ff9a3be1d7 Mon Sep 17 00:00:00 2001 From: shoverbj Date: Fri, 12 Jun 2026 13:46:01 -0400 Subject: [PATCH 24/50] update README --- README.md | 50 ++++++++++++++------------------------------------ 1 file changed, 14 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index b3ee600..84be200 100644 --- a/README.md +++ b/README.md @@ -1,46 +1,24 @@ -# The Scouting Units of American Legion Post 331 +# The Scouting Units of American Legion Post 331 Website ⚜️ -This repository hosts the content for the Website for the Scouting Units of -American Legion Post 331. +Welcome to the official code repository for The Scouting Units of American Legion Post 331. -## Updating Site +## 🎯 Purpose of this Repository -### Homepage +This repository hosts the source code, content, and configuration files for our unit's community website. The site is built using [Docusaurus](https://docusaurus.io), a modern static site generator. -Other than images, the homepage should stay fairly static. +The primary goals of this website are to: -#### Carousel +- **Inform Families:** Provide parents and scouts with easy access to schedules, + packing lists, policies and information regarding our units. +- **Streamline Operations:** Host downloadable permission slips, medical forms, and resource links. +- **Share Our Story:** Show prospective members what makes our units a great place to grow, learn, and experience the outdoors. -The carousel pictures are stored in `/static/img/carousel/`. The files shall be -jpg images. When replacing pictures, keep the names the same (older photos are -automatically archived in the git repo). +--- -#### Feature Cards +## ✍️ How to Contribute -The feature card pictures are stored in `/static/img/feature-cards/`. The files -should be jpg images. When replacing the pictures, keep the name the same. +We encourage scouts, parents, and adult leaders to help keep our website accurate and up-to-date! -### Docs +To protect our website's integrity and layout, all updates—including fixing typos, adding calendar events, or uploading new documents—must follow our standard workflow. -Each unit has its own docs locations. All of these are "markdown" files. Adding -documents is as simple as adding the new markdown file to the folder in -`/docs/` directory. - -### Blog - -These are also all markdown files. Tags can be added to associate it with a particular -unit. - -## List of TODOs - -- [ ] Determine list of domains (subdomains can also be used) - -### Homepage TODOs - -- [ ] Determine what links should be in navbar -- [ ] Determine best pictures - -### Docs pages TODOs - -- [ ] Collect content for all units that they want displayed -- [ ] Determine organization (what docs should be in general category) +Please read our **[CONTRIBUTING.md](CONTRIBUTING.md)** document for full instructions on setting up your local environment, writing markdown content, and submitting your changes for review. From 60c11d417008aadf793aefbd7acb6f35a94f0db8 Mon Sep 17 00:00:00 2001 From: shoverbj Date: Fri, 12 Jun 2026 13:48:44 -0400 Subject: [PATCH 25/50] update readme --- README.md | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 84be200..245c099 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,35 @@ -# The Scouting Units of American Legion Post 331 Website ⚜️ +# The Scouting Units of American Legion Post 331 ⚜️ -Welcome to the official code repository for The Scouting Units of American Legion Post 331. +Welcome to the official code repository for The Scouting Units of American +Legion Post 331 website. ## 🎯 Purpose of this Repository -This repository hosts the source code, content, and configuration files for our unit's community website. The site is built using [Docusaurus](https://docusaurus.io), a modern static site generator. +This repository hosts the source code, content, and configuration files for our +unit's community website. The site is built using +[Docusaurus](https://docusaurus.io), a modern static site generator. This +platform makes content creation easy and straight forward. The primary goals of this website are to: - **Inform Families:** Provide parents and scouts with easy access to schedules, packing lists, policies and information regarding our units. -- **Streamline Operations:** Host downloadable permission slips, medical forms, and resource links. -- **Share Our Story:** Show prospective members what makes our units a great place to grow, learn, and experience the outdoors. +- **Streamline Operations:** Host downloadable permission slips, medical forms, + and resource links. +- **Share Our Story:** Show prospective members what makes our units a great + place to grow, learn, and experience the outdoors. --- ## ✍️ How to Contribute -We encourage scouts, parents, and adult leaders to help keep our website accurate and up-to-date! +We encourage scouts, parents, and adult leaders to help keep our website +accurate and up-to-date! -To protect our website's integrity and layout, all updates—including fixing typos, adding calendar events, or uploading new documents—must follow our standard workflow. +To protect our website's integrity and layout, all updates—including fixing +typos, adding calendar events, or uploading new documents—must follow our +standard workflow. -Please read our **[CONTRIBUTING.md](CONTRIBUTING.md)** document for full instructions on setting up your local environment, writing markdown content, and submitting your changes for review. +Please read our **[CONTRIBUTING.md](CONTRIBUTING.md)** document for full +instructions on setting up your local environment, writing markdown content, and +submitting your changes for review. From 0fecec2616a8ac679d4a44cc5686c5f45f16970c Mon Sep 17 00:00:00 2001 From: shoverbj Date: Fri, 12 Jun 2026 15:05:36 -0400 Subject: [PATCH 26/50] fix typos --- .github/ISSUE_TEMPLATE/01-new-blog-post.yml | 2 +- .github/workflows/create-blog-post.yml | 2 +- .github/workflows/process-new-authors.yml | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/01-new-blog-post.yml b/.github/ISSUE_TEMPLATE/01-new-blog-post.yml index f4e8532..1592d3c 100644 --- a/.github/ISSUE_TEMPLATE/01-new-blog-post.yml +++ b/.github/ISSUE_TEMPLATE/01-new-blog-post.yml @@ -55,7 +55,7 @@ body: If your name is not in the list of authors, please submit a request to add your name to the list of authors here: - [New Author Request](https://github.com/scouting331/scoutSite/issues/new?template=new-blog-author.yml) + [New Author Request](https://github.com/scouting331/scoutSite/issues/new?template=03-new-blog-author.yml) - type: dropdown id: tags attributes: diff --git a/.github/workflows/create-blog-post.yml b/.github/workflows/create-blog-post.yml index d85fe67..c90265b 100644 --- a/.github/workflows/create-blog-post.yml +++ b/.github/workflows/create-blog-post.yml @@ -21,7 +21,7 @@ jobs: id: parse uses: stefanbuck/github-issue-parser@v3 with: - template-path: .github/ISSUE_TEMPLATE/new-blog-post.yml + template-path: .github/ISSUE_TEMPLATE/01-new-blog-post.yml - name: Set up Python uses: actions/setup-python@v5 diff --git a/.github/workflows/process-new-authors.yml b/.github/workflows/process-new-authors.yml index 2ee8db6..e9f4163 100644 --- a/.github/workflows/process-new-authors.yml +++ b/.github/workflows/process-new-authors.yml @@ -22,7 +22,7 @@ jobs: id: parse uses: stefanbuck/github-issue-parser@v3 with: - template-path: .github/ISSUE_TEMPLATE/new-blog-author.yml + template-path: .github/ISSUE_TEMPLATE/03-new-blog-author.yml - name: Set up Python uses: actions/setup-python@v5 @@ -49,7 +49,7 @@ jobs: from PIL import Image, ImageOps AUTHORS_FILE = 'blog/authors.yml' - TEMPLATE_FILE = '.github/ISSUE_TEMPLATE/new-blog-post.yml' + TEMPLATE_FILE = '.github/ISSUE_TEMPLATE/01-new-blog-post.yml' AUTHORS_IMG_DIR = 'static/img/blog/authors' issue_json = os.environ.get("ISSUE_DATA", "{}") From 6f54c95e8b57d12b26f4c5785754d5b5674a4ce8 Mon Sep 17 00:00:00 2001 From: shoverbj Date: Fri, 12 Jun 2026 15:13:39 -0400 Subject: [PATCH 27/50] update versions of actions --- .github/workflows/create-blog-post.yml | 10 +++++----- .github/workflows/process-new-authors.yml | 12 ++++++------ 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/create-blog-post.yml b/.github/workflows/create-blog-post.yml index c90265b..26f2bf8 100644 --- a/.github/workflows/create-blog-post.yml +++ b/.github/workflows/create-blog-post.yml @@ -8,11 +8,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Generate GitHub App Token id: app-token - uses: actions/create-github-app-token@v1 + uses: actions/create-github-app-token@v3 with: app-id: ${{ secrets.APP_ID }} private-key: ${{ secrets.APP_PRIVATE_KEY }} @@ -24,7 +24,7 @@ jobs: template-path: .github/ISSUE_TEMPLATE/01-new-blog-post.yml - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: '3.x' @@ -267,7 +267,7 @@ jobs: - name: Create Pull Request id: cpr - uses: peter-evans/create-pull-request@v6 + uses: peter-evans/create-pull-request@v8 with: token: ${{ steps.app-token.outputs.token }} commit-message: "feat(blog): add new post from issue #${{ github.event.issue.number }}" @@ -281,7 +281,7 @@ jobs: blog - name: Comment on Issue with PR Link if: steps.cpr.outputs.pull-request-number != '' - uses: peter-evans/create-or-update-comment@v4 + uses: peter-evans/create-or-update-comment@v5 with: token: ${{ steps.app-token.outputs.token }} issue-number: ${{ github.event.issue.number }} diff --git a/.github/workflows/process-new-authors.yml b/.github/workflows/process-new-authors.yml index e9f4163..839e69d 100644 --- a/.github/workflows/process-new-authors.yml +++ b/.github/workflows/process-new-authors.yml @@ -9,11 +9,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Repo - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Generate Token id: app-token - uses: actions/create-github-app-token@v1 + uses: actions/create-github-app-token@v3 with: app-id: ${{ secrets.APP_ID }} private-key: ${{ secrets.APP_PRIVATE_KEY }} @@ -25,7 +25,7 @@ jobs: template-path: .github/ISSUE_TEMPLATE/03-new-blog-author.yml - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: '3.x' @@ -165,7 +165,7 @@ jobs: - name: Create Pull Request if: success() - uses: peter-evans/create-pull-request@v6 + uses: peter-evans/create-pull-request@v8 with: token: ${{ steps.app-token.outputs.token }} commit-message: "feat: add new blog author via Issue #${{ github.event.issue.number }}" @@ -184,7 +184,7 @@ jobs: - name: Comment on Success if: success() - uses: peter-evans/create-or-update-comment@v4 + uses: peter-evans/create-or-update-comment@v5 with: token: ${{ steps.app-token.outputs.token }} issue-number: ${{ github.event.issue.number }} @@ -193,7 +193,7 @@ jobs: - name: Handle Duplicate / Failure if: failure() - uses: actions/github-script@v7 + uses: actions/github-script@v9 with: github-token: ${{ steps.app-token.outputs.token }} script: | From 5b397a422b5c0b13dab2ea99784af7c5bf0ff8d5 Mon Sep 17 00:00:00 2001 From: shoverbj Date: Fri, 12 Jun 2026 15:22:13 -0400 Subject: [PATCH 28/50] Updating templates --- .github/ISSUE_TEMPLATE/01-new-blog-post.yml | 17 +++++++++++------ .github/ISSUE_TEMPLATE/03-new-blog-author.yml | 13 +++++++++---- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/01-new-blog-post.yml b/.github/ISSUE_TEMPLATE/01-new-blog-post.yml index 1592d3c..8d8c133 100644 --- a/.github/ISSUE_TEMPLATE/01-new-blog-post.yml +++ b/.github/ISSUE_TEMPLATE/01-new-blog-post.yml @@ -76,7 +76,12 @@ body: id: cover-photo attributes: label: "Cover Photo" - description: "Photo shown at top of your blog post and on the homepage card" + description: | + Photo shown at top of your blog post and on the homepage card + + **NOTE**: There is no way to limit the number of files in this space. Please + only upload 1 photo. If multiple photos are uploaded, the script will + simply choose one photo and discard the rest. validations: required: false accept: ".png, .jpg, .jpeg, .gif, .svg, .webp" @@ -85,14 +90,14 @@ body: attributes: label: "Photo Album" description: | - If you want to include a photo album upload it here. - - Compress the folder of photos as a zip file before uploading. + If you want to include a photo album upload all of the photos you want + included here. - NOTE: If the resulting file is too big (>25 Mb), the photo album will need to be added manually (see webmaster) + **NOTE**: If the file is too big (>25 Mb), the photo will need to be resized + before being uploaded. validations: required: false - accept: ".zip" + accept: ".png, .jpg, .jpeg, .gif, .svg, .webp" - type: textarea id: blog-content attributes: diff --git a/.github/ISSUE_TEMPLATE/03-new-blog-author.yml b/.github/ISSUE_TEMPLATE/03-new-blog-author.yml index 6f924ae..9c356d3 100644 --- a/.github/ISSUE_TEMPLATE/03-new-blog-author.yml +++ b/.github/ISSUE_TEMPLATE/03-new-blog-author.yml @@ -14,7 +14,7 @@ body: description: | Name shown in blog posts for the author - NOTE: If you are a Scout this should be your first name followed by + **NOTE**: If you are a Scout this should be your first name followed by a last initial, **NO LAST NAMES PLEASE** If you are an adult and want your last name shown, feel free to include it" @@ -33,9 +33,14 @@ body: id: image_url attributes: label: Avatar Image - description: "This is the photo that will be associated with you. Please - make sure the image size is relatively small and your face is centered in - the image" + description: | + This is the photo that will be associated with you. Please make sure the + image size is relatively small (500 x 500 pixels is ideal) and your face + is centered in the image. + + **NOTE**: There is no way to limit the number of files in this space. Please + only upload 1 photo. If multiple photos are uploaded, the script will + simply choose one photo and discard the rest. validations: required: false accept: ".png, .jpg, .jpeg, .gif, .svg, .webp" From 2418d28a7ab2a7dc2e29e05a40c46363546322c1 Mon Sep 17 00:00:00 2001 From: shoverbj Date: Sun, 14 Jun 2026 19:27:58 -0400 Subject: [PATCH 29/50] importing pics --- .github/workflows/create-blog-post.yml | 161 +++++++++++++------------ 1 file changed, 81 insertions(+), 80 deletions(-) diff --git a/.github/workflows/create-blog-post.yml b/.github/workflows/create-blog-post.yml index 26f2bf8..939cafb 100644 --- a/.github/workflows/create-blog-post.yml +++ b/.github/workflows/create-blog-post.yml @@ -50,6 +50,19 @@ jobs: from dateutil import parser from PIL import Image, ImageOps + # --- Helper to extract raw URLs from GitHub Markdown Links --- + def extract_url(text): + if not text or text == "null" or text == "_No response_": + return None + # Matches markdown urls like ![text](url) or [text](url) + match = re.search(r'\]\((https?://[^\s\)]+)\)', text) + if match: + return match.group(1) + # Fallback if it is already a plain URL + if text.strip().startswith(('http://', 'https://')): + return text.strip() + return None + # --- 1. INTEGRATED OPTIMIZER ENGINE CODE --- def optimize_convert_and_hash_images(input_dir, output_dir, max_size=(1920, 1080), quality=80, keep_original_names=False): if not os.path.exists(input_dir): @@ -92,9 +105,11 @@ jobs: if os.path.exists(input_path): os.remove(input_path) - with open(os.path.join(root, f"{file}.ref"), "r") as ref_f: - orig_url = ref_f.read().strip() - url_to_new_path_map[orig_url] = output_file_name + ref_path = os.path.join(root, f"{file}.ref") + if os.path.exists(ref_path): + with open(ref_path, "r") as ref_f: + orig_url = ref_f.read().strip() + url_to_new_path_map[orig_url] = output_file_name print(f"[Processed & Purged] {file} -> {output_file_name}") except Exception as e: @@ -135,7 +150,6 @@ jobs: else: authors = [to_kebab(str(raw_authors))] - # This section handles parsing whether options are flat strings or structured maps raw_tags = data.get("tags", "") if isinstance(raw_tags, str): tags = [to_kebab(t) for t in raw_tags.split(",") if t.strip()] @@ -146,9 +160,10 @@ jobs: web_prefix = f"/img/blog/{blogfilename}" # --- 3. FETCH AND RUN OPTIMIZER ON COVER PHOTO --- - cover_url = data.get("cover-photo") + cover_input = data.get("cover-photo") + cover_url = extract_url(cover_input) cover_line = "" - if cover_url and cover_url != "null" and cover_url != "_No response_": + if cover_url: tmp_cover_dir = "/tmp/raw_cover" os.makedirs(tmp_cover_dir, exist_ok=True) clean_url = cover_url.split("?") @@ -166,10 +181,10 @@ jobs: print(f"Skipping cover photo download. Error: {e}") # --- 4. FETCH AND RUN OPTIMIZER ON ZIP ALBUM SLIDES --- - album_url = data.get("photo-album") - has_album = False - album_line = "" - if album_url and album_url != "null" and album_url != "_No response_": + album_input = data.get("photo-album") + album_url = extract_url(album_input) + album_images_paths = [] + if album_url: tmp_slides_dir = "/tmp/raw_slides" os.makedirs(tmp_slides_dir, exist_ok=True) zip_path = "album.zip" @@ -179,91 +194,77 @@ jobs: zip_ref.extractall(tmp_slides_dir) os.remove(zip_path) - for root, _, files in os.walk(tmp_slides_dir): - for file in files: + # Create tracking references for zip content files + for r, _, f_list in os.walk(tmp_slides_dir): + for file in f_list: if file.lower().endswith((".jpg", ".jpeg", ".png", ".webp")): - with open(os.path.join(root, f"{file}.ref"), "w") as ref_f: - ref_f.write("album") + with open(os.path.join(r, f"{file}.ref"), "w") as ref_f: + ref_f.write(file) - slides_target_folder = os.path.join(static_folder, "slides") - optimize_convert_and_hash_images(tmp_slides_dir, slides_target_folder, keep_original_names=True) - has_album = True - album_line = f"" + album_map = optimize_convert_and_hash_images(tmp_slides_dir, static_folder, keep_original_names=False) + album_images_paths = [f"{web_prefix}/{v}" for v in album_map.values()] except Exception as e: - print(f"Skipping album download. Error: {e}") - - # --- 5. PARSE & PROCESS INLINE CONTENT PHOTOS --- - body_text = data.get("blog-content", "").strip() - - inline_markdown_urls = re.findall(r'!\[.*?\]\((.*?)\)', body_text) - inline_html_urls = re.findall(r']*src=["\'](.*?)["\']', body_text) - all_inline_urls = list(set(inline_markdown_urls + inline_html_urls)) + print(f"Skipping zip photo album extract. Error: {e}") + + # --- 5. PARSE AND DOWNLOAD IMAGES INSIDE BLOG CONTENT --- + blog_content = data.get("blog-content", "") + # Find all markdown image URLs inside the text editor field + inline_matches = re.findall(r'!\[.*?\]\(((https?://[^\s\)]+))\)', blog_content) - if all_inline_urls: + if inline_matches: tmp_inline_dir = "/tmp/raw_inline" os.makedirs(tmp_inline_dir, exist_ok=True) - print(f"[Inline Images Detected] Found {len(all_inline_urls)} embedded image assets.") - for idx, img_url in enumerate(all_inline_urls): - if not img_url.startswith("http") or img_url == "null": - continue + for index, (full_match_url, clean_match_url) in enumerate(inline_matches): + clean_url = clean_match_url.split("?")[0] + _, ext = os.path.splitext(clean_url) + if not ext: + ext = ".png" + inline_filename = f"inline_{index}{ext}" + inline_filepath = os.path.join(tmp_inline_dir, inline_filename) + try: - clean_img_url = img_url.split("?") - _, ext = os.path.splitext(clean_img_url[0]) - if not ext: - ext = ".jpg" - - filename = f"inline_img_{idx}{ext}" - full_tmp_path = os.path.join(tmp_inline_dir, filename) - - urllib.request.urlretrieve(img_url, full_tmp_path) - with open(f"{full_tmp_path}.ref", "w") as ref_f: - ref_f.write(img_url) + urllib.request.urlretrieve(clean_match_url, inline_filepath) + with open(f"{inline_filepath}.ref", "w") as ref_f: + ref_f.write(clean_match_url) except Exception as e: - print(f"Failed to pull inline asset reference: {img_url}. Error: {e}") + print(f"Failed downloading inline image {clean_match_url}: {e}") - url_to_filename_map = optimize_convert_and_hash_images(tmp_inline_dir, static_folder, keep_original_names=False) + inline_map = optimize_convert_and_hash_images(tmp_inline_dir, static_folder, keep_original_names=False) - for original_url, new_filename in url_to_filename_map.items(): - optimized_web_path = f"{web_prefix}/{new_filename}" - body_text = body_text.replace(original_url, optimized_web_path) - print(f"[Rewriting Asset Path] {original_url} -> {optimized_web_path}") + # Swap original markdown URLs with the newly generated WebP image routes + for orig_url, webp_filename in inline_map.items(): + blog_content = blog_content.replace(orig_url, f"{web_prefix}/{webp_filename}") - paragraphs = [p.strip() for p in body_text.split("\n\n") if p.strip()] - content_markdown = "" - if paragraphs: - content_markdown += paragraphs[0] + "\n\n{/* truncate */}" - if len(paragraphs) > 1: - content_markdown += "\n\n" + "\n\n".join(paragraphs[1:]) - - # --- 6. COMPILE FILE VIA CLEAN SEQUENTIAL LIST AND \n JOINS --- - authors_str = ", ".join([f'"{a}"' for a in authors]) - tags_str = ", ".join([f'"{t}"' for t in tags]) - lines = [ - "---", - f'title: "{safe_title}"', - f"date: {date_str}", - f"authors: [{authors_str}]", - f"tags: [{tags_str}]", - "---", - "" - ] - if has_album: - lines.append("import PhotoAlbumGallery from '@site/src/components/PhotoAlbumGallery';\n") - if cover_line: - lines.append(cover_line + "\n") - lines.append(content_markdown) - if has_album and album_line: - lines.append("\n" + album_line) - - file_content = "\n".join(lines).strip() + "\n" + # --- 6. WRITE OUT THE MDX POST FILE --- os.makedirs("blog", exist_ok=True) + md_filepath = f"blog/{blogfilename}.md" + + front_matter_authors = ", ".join(authors) + front_matter_tags = ", ".join(tags) - with open(f"blog/{blogfilename}.mdx", "w", encoding="utf-8") as f: - f.write(file_content) + # Render photo album stack if pictures were provided + album_markdown = "" + if album_images_paths: + album_markdown = "\n\n## Photo Album\n" + "\n".join([f"![Album Image]({path})" for path in album_images_paths]) + + with open(md_filepath, "w", encoding="utf-8") as md_file: + md_file.write(f"---\n") + md_file.write(f"title: \"{safe_title}\"\n") + md_file.write(f"date: {date_str}\n") + if authors: + md_file.write(f"authors: [{front_matter_authors}]\n") + if tags: + md_file.write(f"tags: [{front_matter_tags}]\n") + md_file.write(f"---\n\n") + + if cover_line: + md_file.write(f"{cover_line}\n\n") + + md_file.write(f"{blog_content}") + md_file.write(f"{album_markdown}\n") - with open(os.environ["GITHUB_ENV"], "a") as env_file: - env_file.write(f"BLOG_FILENAME={blogfilename}\n") + print(f"Successfully generated blog file at: {md_filepath}") - name: Create Pull Request id: cpr From 8f3dc78fa3c1a180ea5ce36665e2fddc8a4a4a5a Mon Sep 17 00:00:00 2001 From: shoverbj Date: Mon, 15 Jun 2026 08:53:06 -0400 Subject: [PATCH 30/50] bug fixes and clean up of blog post automation --- .github/scripts/generate_post.py | 404 +++++++++++++++++++++++++ .github/workflows/create-blog-post.yml | 228 +------------- 2 files changed, 405 insertions(+), 227 deletions(-) create mode 100644 .github/scripts/generate_post.py diff --git a/.github/scripts/generate_post.py b/.github/scripts/generate_post.py new file mode 100644 index 0000000..9079140 --- /dev/null +++ b/.github/scripts/generate_post.py @@ -0,0 +1,404 @@ +""" +Docusaurus Blog Post Generator and Asset Optimizer. + +This module automates the generation of MDX blog posts for Docusaurus from +structured GitHub Issue Form JSON data. It extracts front matter details +(title, date, authors, tags), downloads external asset attachments (cover +photos, inline body images, and multi-file photo albums), converts and compresses +images into responsive WebP formats, and injects custom React components. + +Workflow Steps: + 1. Parses form data and normalizes string strings into URL keys (kebab-case). + 2. Downloads and runs a multi-format image optimizer on cover configurations. + 3. Traverses inline markdown text to collect, downscale, and swap out local assets. + 4. Downloads sequential photo attachments and places them inside an album asset directory. + 5. Calculates text positions to cleanly inject a Docusaurus preview truncation marker. + 6. Compiles and exports a clean MDX file to the repository's native `blog/` folder. + +Environment Variables: + ISSUE_JSON (str): A stringified JSON object generated by `stefanbuck/github-issue-parser` + containing the raw form field mappings. + +Requirements: + - Pillow (PIL) + - python-dateutil + +File Structure Impact: + - Creates layout files at: `blog/{YYYY-MM-DD}-{slug}.mdx` + - Stores optimized assets at: `static/img/blog/{YYYY-MM-DD}-{slug}/` +""" +import os +import json +import re +import hashlib +import urllib.request +from io import BytesIO +from datetime import datetime +from dateutil import parser +from PIL import Image, ImageOps + +# --- Helper functions --- +def optimize_convert_and_hash_images(input_dir, output_dir, max_size=(1920, 1080), quality=80, keep_original_names=False): + """ + Optimizes, resizes, and converts images in an input directory to WebP format, + saving them to an output directory while optionally renaming them using MD5 hashes. + + This function traverses an input folder structure, processes supported images + (.jpg, .jpeg, .png, .webp), applies EXIF orientation fixes, resizes them to + fit within a maximum bounding box, and saves the output. It purges the original + files from disk upon successful processing. It also links processed files back + to their original reference URLs if matching '.ref' sidecar files are present. + + Args: + input_dir (str): Path to the directory containing raw source images. + output_dir (str): Path to the destination directory where WebP images will be saved. + max_size (tuple of int, optional): Maximum (width, height) bounding box for resizing + using a LANCZOS filter. Defaults to (1920, 1080). + quality (int, optional): Compression quality parameter for WebP export, + ranging from 1 to 100. Defaults to 80. + keep_original_names (bool, optional): If True, retains the original file base name + with a '.webp' extension. If False, names the file using an MD5 hash of its + optimized data stream to ensure uniqueness and prevent duplicates. Defaults to False. + + Returns: + dict: A dictionary mapping original source URLs (extracted from corresponding '.ref' + files) to the newly generated file names (e.g., {'https://example.com': 'img_abc123.webp'}). + + Raises: + No explicit exceptions are raised. Errors encountered during individual file processing + or file mutations are intercepted and printed to standard output to allow the loop to continue. + """ + if not os.path.exists(input_dir): + return {} + os.makedirs(output_dir, exist_ok=True) + valid_extensions = (".jpg", ".jpeg", ".png", ".webp") + url_to_new_path_map = {} + + for root, _, files in os.walk(input_dir): + for file in files: + if file.lower().endswith(valid_extensions): + input_path = os.path.join(root, file) + relative_path = os.path.relpath(root, input_dir) + target_folder = os.path.normpath(os.path.join(output_dir, relative_path)) + os.makedirs(target_folder, exist_ok=True) + try: + with Image.open(input_path) as img: + img = ImageOps.exif_transpose(img) + if img.mode in ("P", "CMYK"): + img = img.convert("RGBA") + img.thumbnail(max_size, Image.Resampling.LANCZOS) + buffer = BytesIO() + img.save(buffer, format="WEBP", quality=quality) + optimized_data = buffer.getvalue() + + if keep_original_names: + base_name, _ = os.path.splitext(file) + output_file_name = f"{base_name}.webp" + else: + hasher = hashlib.md5(optimized_data) + content_hash = hasher.hexdigest() + output_file_name = f"img_{content_hash}.webp" + + output_path = os.path.join(target_folder, output_file_name) + + if not os.path.exists(output_path): + with open(output_path, "wb") as f: + f.write(optimized_data) + + if os.path.exists(input_path): + os.remove(input_path) + + ref_path = os.path.join(root, f"{file}.ref") + if os.path.exists(ref_path): + with open(ref_path, "r") as ref_f: + orig_url = ref_f.read().strip() + url_to_new_path_map[orig_url] = output_file_name + + print(f"[Processed & Purged] {file} -> {output_file_name}") + except Exception as e: + print(f"Failed to process {file}. Error: {e}") + return url_to_new_path_map + +def to_kebab(text): + """ + Converts a text string into a clean, lowercased kebab-case format + suitable for slugs, URLs, or file names. + + The function strips leading/trailing whitespace, converts characters + to lowercase, removes non-alphanumeric symbols (excluding spaces + and hyphens), collapses consecutive spaces or hyphens into a single + dash, and trims leading or trailing dashes from the final output. + + Args: + text (str or None): The input text string to be slugified. + + Returns: + str: The transformed kebab-case string, or an empty string + if the input is empty or evaluates to None. + + Examples: + >>> to_kebab("Hello World!") + 'hello-world' + + >>> to_kebab(" My Awesome---Blog Post ") + 'my-awesome-blog-post' + + >>> to_kebab(None) + '' + """ + if not text: + return "" + text = text.lower().strip() + text = re.sub(r"[^a-z0-9\s-]", "", text) + return re.sub(r"[\s-]+", "-", text).strip("-") + +def main(): + """ + Executes the end-to-end extraction, asset optimization, and generation pipeline. + + This function acts as the orchestrator for the script. It loads the source JSON + payload from the environment variables, processes author metadata, downloads + and compresses all remote image attachments into WebP files, calculates layout + break positions for preview truncation, and writes the final, formatted MDX + blog post directly to the workspace directory. + + Supported Form Input Fields (extracted from ISSUE_JSON): + title (str): The raw title text used for front matter and file naming. + date (str, optional): Overriding target creation date. Defaults to current system date. + authors (str): Comma-separated list of author identifiers. + tags (str): Comma-separated list of classification metadata tags. + cover-photo (str, optional): Markdown link or plain URL pointing to a header image. + photo-album (str, optional): Clustered markdown links of gallery image attachments. + blog-content (str): Raw Markdown core narrative containing text and inline graphics. + + Returns: + None + + Raises: + KeyError: If the 'ISSUE_JSON' environment variable is entirely missing. + json.JSONDecodeError: If the provided configuration string contains malformed JSON data. + """ + # --- SETUP FORMS & METADATA --- + data = json.loads(os.environ["ISSUE_JSON"]) + + # Get safe_title and raw_title from 'title' + raw_title = data.get("title", "Untitled Post").strip() + safe_title = raw_title.replace('"', '\\"') + + # Get date_str from 'date' (convert whatever format was entered to YYYY-MM-DD) + form_date = data.get("date", "").strip() + current_time = datetime.now() + date_str = current_time.strftime("%Y-%m-%d") + + if form_date and form_date != "null" and form_date != "_No response_": + try: + year_anchor = datetime(current_time.year, 1, 1) + parsed_date = parser.parse(form_date, default=year_anchor, fuzzy=True) + date_str = parsed_date.strftime("%Y-%m-%d") + except Exception as e: + print(f"[Date Parsing Alert] Fallback used. Error: {e}") + + # Get blogfilename from date_str and slugified raw_title + slug = to_kebab(raw_title) + blogfilename = f"{date_str}-{slug}" + + # Get list of raw_authors from 'authors' + raw_authors = data.get("authors", "") + if isinstance(raw_authors, str): + authors = [to_kebab(a) for a in raw_authors.split(",") if a.strip()] + else: + authors = [to_kebab(str(raw_authors))] + + # Get raw_tags from kebabed tags (Unit number) + raw_tags = data.get("tags", "") + if isinstance(raw_tags, str): + tags = [to_kebab(t) for t in raw_tags.split(",") if t.strip()] + else: + tags = [to_kebab(str(raw_tags))] + + # File locations for processed images + static_folder = f"static/img/blog/{blogfilename}" + web_prefix = f"/img/blog/{blogfilename}" + + # --- FETCH AND RUN OPTIMIZER ON COVER PHOTO --- + cover_input = data.get("cover-photo") + cover_match = re.search(r'\]\((https?://[^\s\)]+)\)', cover_input) + cover_url = cover_match.group(1) if cover_match else (cover_input.strip() if cover_input.strip().startswith(('http://', 'https://')) else None) + + cover_line = "" + if cover_url and cover_url != "null" and cover_url != "_No response_": + tmp_cover_dir = "/tmp/raw_cover" + os.makedirs(tmp_cover_dir, exist_ok=True) + clean_url = cover_url.split("?") + _, ext = os.path.splitext(clean_url[0]) + if not ext: + ext = ".jpg" + raw_cover_path = os.path.join(tmp_cover_dir, f"cover{ext}") + try: + urllib.request.urlretrieve(cover_url, raw_cover_path) + with open(f"{raw_cover_path}.ref", "w") as ref_f: + ref_f.write("cover") + optimize_convert_and_hash_images(tmp_cover_dir, static_folder, keep_original_names=True) + cover_line = f"![Cover Picture]({web_prefix}/cover.webp)" + except Exception as e: + print(f"Skipping cover photo download. Error: {e}") + + # --- FETCH AND RUN OPTIMIZER ON ALL ALBUM SLIDES --- + album_input = data.get("photo-album", "") + # Extract all markdown image links from the multi-file upload field + album_matches = re.findall(r'!\[.*?\]\(((https?://[^\s\)]+))\)', album_input) + has_album = False + + if album_matches: + tmp_slides_dir = "/tmp/raw_slides" + os.makedirs(tmp_slides_dir, exist_ok=True) + + # Nest the optimized slides directly inside the target dynamic subfolder path + slides_static_folder = os.path.join(static_folder, "slides") + + # Download each individual photo into the temporary processing directory + for index, (full_match_url, clean_match_url) in enumerate(album_matches): + clean_url = clean_match_url.split("?")[0] + _, ext = os.path.splitext(clean_url) + if not ext: + ext = ".png" + + # Use an index-based name to keep them unique during raw staging + album_filename = f"album_{index}{ext}" + album_filepath = os.path.join(tmp_slides_dir, album_filename) + + try: + urllib.request.urlretrieve(clean_match_url, album_filepath) + + # Create the required sidecar reference file for tracking + with open(f"{album_filepath}.ref", "w") as ref_f: + ref_f.write(clean_match_url) + except Exception as e: + print(f"Failed downloading album image {clean_match_url}: {e}") + + # Process and optimize the directory full of individual files + optimize_convert_and_hash_images(tmp_slides_dir, static_folder, keep_original_names=False) + has_album = True + + # --- PARSE AND DOWNLOAD IMAGES INSIDE BLOG CONTENT --- + blog_content = data.get("blog-content", "") + # Find all markdown image URLs inside the text editor field + inline_matches = re.findall(r'!\[.*?\]\(((https?://[^\s\)]+))\)', blog_content) + + if inline_matches: + tmp_inline_dir = "/tmp/raw_inline" + os.makedirs(tmp_inline_dir, exist_ok=True) + + for index, (full_match_url, clean_match_url) in enumerate(inline_matches): + clean_url = clean_match_url.split("?") + _, ext = os.path.splitext(clean_url[0]) + if not ext: + ext = ".png" + inline_filename = f"inline_{index}{ext}" + inline_filepath = os.path.join(tmp_inline_dir, inline_filename) + + try: + urllib.request.urlretrieve(clean_match_url, inline_filepath) + with open(f"{inline_filepath}.ref", "w") as ref_f: + ref_f.write(clean_match_url) + except Exception as e: + print(f"Failed downloading inline image {clean_match_url}: {e}") + + inline_map = optimize_convert_and_hash_images(tmp_inline_dir, static_folder, keep_original_names=False) + + # Swap original markdown URLs with the newly generated WebP image routes + for orig_url, webp_filename in inline_map.items(): + blog_content = blog_content.replace(orig_url, f"{web_prefix}/{webp_filename}") + + # --- WRITE OUT THE MDX POST FILE --- + os.makedirs("blog", exist_ok=True) + mdx_filepath = f"blog/{blogfilename}.mdx" + + front_matter_authors = ", ".join(authors) + front_matter_tags = ", ".join(tags) + + # Inject the dynamic PhotoAlbumGallery component into the template if an album was uploaded + import_line = "" + album_component_markdown = "" + if has_album: + import_line = "import PhotoAlbumGallery from '@site/src/components/PhotoAlbumGallery';\n\n" + album_component_markdown = f"""\n\n## Photo Album\n\n\n""" + + # Handle Docusaurus preview truncation, ignoring non-text layout blocks + # Split by double newlines to isolate paragraphs + MAX_PARAGRAPH_CHARS = 350 # Fallback limit for excessively long paragraphs + paragraphs = blog_content.split("\n\n") + + first_text_index = None + has_subsequent_content = False + + # Step A: Locate the first true text block that isn't an image, component, or import statement + for idx, p in enumerate(paragraphs): + stripped_p = p.strip() + + # Combined skip condition for empty blocks, images, code, lists, and headings + if (not stripped_p or + stripped_p.startswith(("![", " MAX_PARAGRAPH_CHARS: + # Find the closest sentence end near our maximum character threshold + # It scans for periods, exclamation marks, or question marks followed by a space + sentence_ends = [m.end() for m in re.finditer(r'[\.\!\?]\s', target_paragraph[:MAX_PARAGRAPH_CHARS + 50])] + + if sentence_ends: + # Split at the last complete sentence within our threshold window + split_point = sentence_ends[-1] + truncated_part = target_paragraph[:split_point].strip() + remaining_part = target_paragraph[split_point:].strip() + + # Re-structure the paragraph to house the tag internally + paragraphs[first_text_index] = f"{truncated_part}\n\n{{/* truncate */}}\n\n{remaining_part}" + else: + # Hard fallback if no punctuation is found: split exactly at the character count + truncated_part = target_paragraph[:MAX_PARAGRAPH_CHARS].strip() + remaining_part = target_paragraph[MAX_PARAGRAPH_CHARS:].strip() + paragraphs[first_text_index] = f"{truncated_part}...\n\n{{/* truncate */}}\n\n...{remaining_part}" + + # If the paragraph is normal length and there's more text coming later, append tag after it + elif has_subsequent_content: + paragraphs.insert(first_text_index + 1, "{/* truncate */}") + + blog_content = "\n\n".join(paragraphs) + + with open(mdx_filepath, "w", encoding="utf-8") as mdx_file: + mdx_file.write(f"---\n") + mdx_file.write(f"title: \"{safe_title}\"\n") + mdx_file.write(f"date: {date_str}\n") + if authors: + mdx_file.write(f"authors: [{front_matter_authors}]\n") + if tags: + mdx_file.write(f"tags: [{front_matter_tags}]\n") + mdx_file.write(f"---\n\n") + if import_line: + mdx_file.write(import_line) + if cover_line: + mdx_file.write(f"{cover_line}\n\n") + mdx_file.write(f"{blog_content}") + mdx_file.write(album_component_markdown) + + print(f"Successfully generated blog file at: {mdx_filepath}") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/.github/workflows/create-blog-post.yml b/.github/workflows/create-blog-post.yml index 939cafb..45cc2d8 100644 --- a/.github/workflows/create-blog-post.yml +++ b/.github/workflows/create-blog-post.yml @@ -37,234 +37,8 @@ jobs: id: process env: ISSUE_JSON: ${{ steps.parse.outputs.jsonString }} - shell: python run: | - import os - import json - import re - import hashlib - import urllib.request - import zipfile - from io import BytesIO - from datetime import datetime - from dateutil import parser - from PIL import Image, ImageOps - - # --- Helper to extract raw URLs from GitHub Markdown Links --- - def extract_url(text): - if not text or text == "null" or text == "_No response_": - return None - # Matches markdown urls like ![text](url) or [text](url) - match = re.search(r'\]\((https?://[^\s\)]+)\)', text) - if match: - return match.group(1) - # Fallback if it is already a plain URL - if text.strip().startswith(('http://', 'https://')): - return text.strip() - return None - - # --- 1. INTEGRATED OPTIMIZER ENGINE CODE --- - def optimize_convert_and_hash_images(input_dir, output_dir, max_size=(1920, 1080), quality=80, keep_original_names=False): - if not os.path.exists(input_dir): - return {} - os.makedirs(output_dir, exist_ok=True) - valid_extensions = (".jpg", ".jpeg", ".png", ".webp") - url_to_new_path_map = {} - - for root, _, files in os.walk(input_dir): - for file in files: - if file.lower().endswith(valid_extensions): - input_path = os.path.join(root, file) - relative_path = os.path.relpath(root, input_dir) - target_folder = os.path.normpath(os.path.join(output_dir, relative_path)) - os.makedirs(target_folder, exist_ok=True) - try: - with Image.open(input_path) as img: - img = ImageOps.exif_transpose(img) - if img.mode in ("P", "CMYK"): - img = img.convert("RGBA") - img.thumbnail(max_size, Image.Resampling.LANCZOS) - buffer = BytesIO() - img.save(buffer, format="WEBP", quality=quality) - optimized_data = buffer.getvalue() - - if keep_original_names: - base_name, _ = os.path.splitext(file) - output_file_name = f"{base_name}.webp" - else: - hasher = hashlib.md5(optimized_data) - content_hash = hasher.hexdigest() - output_file_name = f"img_{content_hash}.webp" - - output_path = os.path.join(target_folder, output_file_name) - - if not os.path.exists(output_path): - with open(output_path, "wb") as f: - f.write(optimized_data) - - if os.path.exists(input_path): - os.remove(input_path) - - ref_path = os.path.join(root, f"{file}.ref") - if os.path.exists(ref_path): - with open(ref_path, "r") as ref_f: - orig_url = ref_f.read().strip() - url_to_new_path_map[orig_url] = output_file_name - - print(f"[Processed & Purged] {file} -> {output_file_name}") - except Exception as e: - print(f"Failed to process {file}. Error: {e}") - return url_to_new_path_map - - # --- 2. SETUP FORMS & METADATA --- - data = json.loads(os.environ["ISSUE_JSON"]) - - raw_title = data.get("title", "Untitled Post").strip() - safe_title = raw_title.replace('"', '\\"') - - form_date = data.get("date", "").strip() - current_time = datetime.now() - date_str = current_time.strftime("%Y-%m-%d") - - if form_date and form_date != "null" and form_date != "_No response_": - try: - year_anchor = datetime(current_time.year, 1, 1) - parsed_date = parser.parse(form_date, default=year_anchor, fuzzy=True) - date_str = parsed_date.strftime("%Y-%m-%d") - except Exception as e: - print(f"[Date Parsing Alert] Fallback used. Error: {e}") - - def to_kebab(text): - if not text: - return "" - text = text.lower().strip() - text = re.sub(r"[^a-z0-9\s-]", "", text) - return re.sub(r"[\s-]+", "-", text).strip("-") - - slug = to_kebab(raw_title) - blogfilename = f"{date_str}-{slug}" - - raw_authors = data.get("authors", "") - if isinstance(raw_authors, str): - authors = [to_kebab(a) for a in raw_authors.split(",") if a.strip()] - else: - authors = [to_kebab(str(raw_authors))] - - raw_tags = data.get("tags", "") - if isinstance(raw_tags, str): - tags = [to_kebab(t) for t in raw_tags.split(",") if t.strip()] - else: - tags = [to_kebab(str(raw_tags))] - - static_folder = f"static/img/blog/{blogfilename}" - web_prefix = f"/img/blog/{blogfilename}" - - # --- 3. FETCH AND RUN OPTIMIZER ON COVER PHOTO --- - cover_input = data.get("cover-photo") - cover_url = extract_url(cover_input) - cover_line = "" - if cover_url: - tmp_cover_dir = "/tmp/raw_cover" - os.makedirs(tmp_cover_dir, exist_ok=True) - clean_url = cover_url.split("?") - _, ext = os.path.splitext(clean_url[0]) - if not ext: - ext = ".jpg" - raw_cover_path = os.path.join(tmp_cover_dir, f"cover{ext}") - try: - urllib.request.urlretrieve(cover_url, raw_cover_path) - with open(f"{raw_cover_path}.ref", "w") as ref_f: - ref_f.write("cover") - optimize_convert_and_hash_images(tmp_cover_dir, static_folder, keep_original_names=True) - cover_line = f"![Cover Picture]({web_prefix}/cover.webp)" - except Exception as e: - print(f"Skipping cover photo download. Error: {e}") - - # --- 4. FETCH AND RUN OPTIMIZER ON ZIP ALBUM SLIDES --- - album_input = data.get("photo-album") - album_url = extract_url(album_input) - album_images_paths = [] - if album_url: - tmp_slides_dir = "/tmp/raw_slides" - os.makedirs(tmp_slides_dir, exist_ok=True) - zip_path = "album.zip" - try: - urllib.request.urlretrieve(album_url, zip_path) - with zipfile.ZipFile(zip_path, "r") as zip_ref: - zip_ref.extractall(tmp_slides_dir) - os.remove(zip_path) - - # Create tracking references for zip content files - for r, _, f_list in os.walk(tmp_slides_dir): - for file in f_list: - if file.lower().endswith((".jpg", ".jpeg", ".png", ".webp")): - with open(os.path.join(r, f"{file}.ref"), "w") as ref_f: - ref_f.write(file) - - album_map = optimize_convert_and_hash_images(tmp_slides_dir, static_folder, keep_original_names=False) - album_images_paths = [f"{web_prefix}/{v}" for v in album_map.values()] - except Exception as e: - print(f"Skipping zip photo album extract. Error: {e}") - - # --- 5. PARSE AND DOWNLOAD IMAGES INSIDE BLOG CONTENT --- - blog_content = data.get("blog-content", "") - # Find all markdown image URLs inside the text editor field - inline_matches = re.findall(r'!\[.*?\]\(((https?://[^\s\)]+))\)', blog_content) - - if inline_matches: - tmp_inline_dir = "/tmp/raw_inline" - os.makedirs(tmp_inline_dir, exist_ok=True) - - for index, (full_match_url, clean_match_url) in enumerate(inline_matches): - clean_url = clean_match_url.split("?")[0] - _, ext = os.path.splitext(clean_url) - if not ext: - ext = ".png" - inline_filename = f"inline_{index}{ext}" - inline_filepath = os.path.join(tmp_inline_dir, inline_filename) - - try: - urllib.request.urlretrieve(clean_match_url, inline_filepath) - with open(f"{inline_filepath}.ref", "w") as ref_f: - ref_f.write(clean_match_url) - except Exception as e: - print(f"Failed downloading inline image {clean_match_url}: {e}") - - inline_map = optimize_convert_and_hash_images(tmp_inline_dir, static_folder, keep_original_names=False) - - # Swap original markdown URLs with the newly generated WebP image routes - for orig_url, webp_filename in inline_map.items(): - blog_content = blog_content.replace(orig_url, f"{web_prefix}/{webp_filename}") - - # --- 6. WRITE OUT THE MDX POST FILE --- - os.makedirs("blog", exist_ok=True) - md_filepath = f"blog/{blogfilename}.md" - - front_matter_authors = ", ".join(authors) - front_matter_tags = ", ".join(tags) - - # Render photo album stack if pictures were provided - album_markdown = "" - if album_images_paths: - album_markdown = "\n\n## Photo Album\n" + "\n".join([f"![Album Image]({path})" for path in album_images_paths]) - - with open(md_filepath, "w", encoding="utf-8") as md_file: - md_file.write(f"---\n") - md_file.write(f"title: \"{safe_title}\"\n") - md_file.write(f"date: {date_str}\n") - if authors: - md_file.write(f"authors: [{front_matter_authors}]\n") - if tags: - md_file.write(f"tags: [{front_matter_tags}]\n") - md_file.write(f"---\n\n") - - if cover_line: - md_file.write(f"{cover_line}\n\n") - - md_file.write(f"{blog_content}") - md_file.write(f"{album_markdown}\n") - - print(f"Successfully generated blog file at: {md_filepath}") + python .github/scripts/generate_post.py - name: Create Pull Request id: cpr From 6efa88c708489b44da426842d20804a22bbbe3e8 Mon Sep 17 00:00:00 2001 From: shoverbj Date: Wed, 17 Jun 2026 06:53:31 -0400 Subject: [PATCH 31/50] truncate blogs --- blog/2026-06-09-scoutmasters-blog.mdx | 4 +++- blog/2026-06-10-scoutmasters-blog.mdx | 6 +++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/blog/2026-06-09-scoutmasters-blog.mdx b/blog/2026-06-09-scoutmasters-blog.mdx index d39a404..3c91b95 100644 --- a/blog/2026-06-09-scoutmasters-blog.mdx +++ b/blog/2026-06-09-scoutmasters-blog.mdx @@ -8,11 +8,13 @@ tags: [troop-303] A bit chaotic as many people and appointments have to line up just to be a good day. There was some misalignment, but in the end I got situated in my room, had a PICC line inserted (this goes into a bigger artery/vein for easier access to -the heart's pumping acting. Chemotherapy needs to be delivered into the big A/V +the heart's pumping acting). Chemotherapy needs to be delivered into the big A/V areas so it does not get stuck in the smaller areas. This ensure better delivery to where it is needed. Then rest, as chemotherapy could not begin before the ECHO of the heart to make sure it was healthy and able to receive the chemo. +{/* truncate */} + It was awesome to see Mr. Beasley and to pray with him over me. Scouts, you truly know who cares for you when your on a tough road. We got to talk about the journey so far and even remembered several Scouting events and had a good laugh. diff --git a/blog/2026-06-10-scoutmasters-blog.mdx b/blog/2026-06-10-scoutmasters-blog.mdx index fa3ac6b..df2c276 100644 --- a/blog/2026-06-10-scoutmasters-blog.mdx +++ b/blog/2026-06-10-scoutmasters-blog.mdx @@ -10,7 +10,11 @@ came next which included all of the anti-nausea medications to help get through the chemotherapy. Chemo came at 1500 and was not as bad as I had thought. Fifteen minutes for one, then an hour for the second. Today it was Mr. Darland and Mr. McConnell. Kudos to both of them, hospitals tend to make people nervous -because they don't know what to say or do. To be perfectly honest, just your +because they don't know what to say or do. + +{/* truncate */} + +To be perfectly honest, just your presence makes all the difference in the world. We laughed and enjoy time together, and Mrs. McConnell made me some of her famous baked oyster crackers and seasonings. We refer to this as Scoutmaster Crack, just because of how good From 3b4a29e11fc56aec160bfd8ef5fc3c59203c3a02 Mon Sep 17 00:00:00 2001 From: shoverbj Date: Wed, 17 Jun 2026 08:13:18 -0400 Subject: [PATCH 32/50] adding new document template --- .github/ISSUE_TEMPLATE/02-new-document.md | 7 -- .github/ISSUE_TEMPLATE/02-new-document.yml | 66 +++++++++++++++++++ .../{general-docs => general}/health-form.mdx | 0 .../helpful-links.mdx | 0 .../merit-badge-counselor.mdx | 0 docusaurus.config.js | 4 +- sidebarDocs.js | 4 +- 7 files changed, 70 insertions(+), 11 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/02-new-document.md create mode 100644 .github/ISSUE_TEMPLATE/02-new-document.yml rename docs/{general-docs => general}/health-form.mdx (100%) rename docs/{general-docs => general}/helpful-links.mdx (100%) rename docs/{general-docs => general}/merit-badge-counselor.mdx (100%) diff --git a/.github/ISSUE_TEMPLATE/02-new-document.md b/.github/ISSUE_TEMPLATE/02-new-document.md deleted file mode 100644 index d46c59e..0000000 --- a/.github/ISSUE_TEMPLATE/02-new-document.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -name: New Document -about: Add a new document to the Unit Sites -title: "" -labels: documents -assignees: shoverbj ---- diff --git a/.github/ISSUE_TEMPLATE/02-new-document.yml b/.github/ISSUE_TEMPLATE/02-new-document.yml new file mode 100644 index 0000000..98c7f69 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/02-new-document.yml @@ -0,0 +1,66 @@ +name: New Document +description: "Add a new document" +labels: ["docs"] +title: "[New Document]" +body: + - type: markdown + attributes: + value: | + **The title above is just for internal use. Please don't change it** + + # Adding a new document to the site + + Fill out this form and click "Create" below to submit a new document. + After submitting your document, it will be reviewed by the webmaster before + being added to the website. + - type: input + id: title + attributes: + label: "Document Title" + description: | + Title of the proposed document + + This will be displayed at the top of the document. Make sure it is descriptive of what the document is! + placeholder: "New Document" + validations: + required: true + - type: input + id: description + attributes: + label: "Document Description" + description: | + Please provide a single sentence description of what this document is. + placeholder: "This is the packing list/rules/etc..." + validations: + required: true + - type: dropdown + id: tags + attributes: + label: "Unit" + description: | + Select the unit(s) to associate this post with. + + If this document applies to multiple units, select "General Document" + multiple: false + options: + - Troop 303 + - Troop 331 + - Crew 303 + - Pack 303 + - General + validations: + required: true + - type: textarea + id: doc-content + attributes: + label: "Document Text" + description: | + Add the content of your document here. You can include inline + images/files if needed. + placeholder: "Document text here..." + validations: + required: true + - type: markdown + attributes: + value: | + **Please don't edit any of the other options!** diff --git a/docs/general-docs/health-form.mdx b/docs/general/health-form.mdx similarity index 100% rename from docs/general-docs/health-form.mdx rename to docs/general/health-form.mdx diff --git a/docs/general-docs/helpful-links.mdx b/docs/general/helpful-links.mdx similarity index 100% rename from docs/general-docs/helpful-links.mdx rename to docs/general/helpful-links.mdx diff --git a/docs/general-docs/merit-badge-counselor.mdx b/docs/general/merit-badge-counselor.mdx similarity index 100% rename from docs/general-docs/merit-badge-counselor.mdx rename to docs/general/merit-badge-counselor.mdx diff --git a/docusaurus.config.js b/docusaurus.config.js index af15a5d..06ccd4f 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -178,7 +178,7 @@ const config = { items: [ { label: "Documents", - to: "/docs/general-docs", + to: "/docs/general", }, { label: "Blog", @@ -186,7 +186,7 @@ const config = { }, { label: "Helpful Links", - to: "/docs/general-docs/helpful-links" + to: "/docs/general/helpful-links" }, ], }, diff --git a/sidebarDocs.js b/sidebarDocs.js index 416d9ee..d317137 100644 --- a/sidebarDocs.js +++ b/sidebarDocs.js @@ -23,12 +23,12 @@ export default { type: 'generated-index', title: 'General Documents', description: 'General documents applicable to multiple units', - slug: '/general-docs' + slug: '/general' }, items: [ { type: 'autogenerated', - dirName: 'general-docs' + dirName: 'general' } ] }, From 9194f855e6e81933f522b2765f19384757b63931 Mon Sep 17 00:00:00 2001 From: shoverbj Date: Wed, 17 Jun 2026 21:34:59 -0400 Subject: [PATCH 33/50] updates for AI dev --- .github/ISSUE_TEMPLATE/02-new-document.yml | 2 +- .github/scripts/generate_docs.py | 247 +++++++++++++++++++++ .github/scripts/generate_post.py | 3 + .github/workflows/create-doc.yml | 69 ++++++ cookbook/breakfast/biscuit-gravy.mdx | 1 - cookbook/dutch-oven.mdx | 1 - cookbook/index.mdx | 1 - docusaurus.config.js | 41 +++- package-lock.json | 13 ++ package.json | 1 + src/components/BlogCard/index.jsx | 1 - src/css/custom.css | 89 ++++++++ static/llms.txt | 102 +++++++++ tmp_llms.md | 61 +++++ 14 files changed, 620 insertions(+), 12 deletions(-) create mode 100644 .github/scripts/generate_docs.py create mode 100644 .github/workflows/create-doc.yml create mode 100644 static/llms.txt create mode 100644 tmp_llms.md diff --git a/.github/ISSUE_TEMPLATE/02-new-document.yml b/.github/ISSUE_TEMPLATE/02-new-document.yml index 98c7f69..125a06b 100644 --- a/.github/ISSUE_TEMPLATE/02-new-document.yml +++ b/.github/ISSUE_TEMPLATE/02-new-document.yml @@ -40,7 +40,7 @@ body: description: | Select the unit(s) to associate this post with. - If this document applies to multiple units, select "General Document" + If this document applies to multiple units, select **General** multiple: false options: - Troop 303 diff --git a/.github/scripts/generate_docs.py b/.github/scripts/generate_docs.py new file mode 100644 index 0000000..46c7799 --- /dev/null +++ b/.github/scripts/generate_docs.py @@ -0,0 +1,247 @@ +""" +Docusaurus Document Generator and Asset Optimizer. + +This module automates the generation of MDX documents for Docusaurus from +structured GitHub Issue Form JSON data. It extracts front matter details +(title, description), downloads external asset attachments (inline body images +and files), and converts and compresses images into responsive WebP formats. + +Workflow Steps: + 1. Parses form data and normalizes string strings into URL keys (kebab-case). + 2. Traverses inline markdown text to collect, downscale, and swap out local assets. + 3. Downloads sequential photo attachments and places them inside an album asset directory. + 4. Compiles and exports a clean MDX file to the repository's native `docs/` folder. + +Environment Variables: + ISSUE_JSON (str): A stringified JSON object generated by `stefanbuck/github-issue-parser` + containing the raw form field mappings. + +Requirements: + - Pillow (PIL) + +File Structure Impact: + - Creates layout files at: `docs/{unit}/{slug}.mdx` + - Stores optimized assets at: `static/img/docs/{unit}/{slug}/` +""" +import os +import json +import re +import hashlib +import urllib.request +from io import BytesIO +from PIL import Image, ImageOps + +# --- Helper functions --- +def optimize_convert_and_hash_images(input_dir, output_dir, max_size=(1920, 1080), quality=80, keep_original_names=False): + """ + Optimizes, resizes, and converts images in an input directory to WebP format, + saving them to an output directory while optionally renaming them using MD5 hashes. + + This function traverses an input folder structure, processes supported images + (.jpg, .jpeg, .png, .webp), applies EXIF orientation fixes, resizes them to + fit within a maximum bounding box, and saves the output. It purges the original + files from disk upon successful processing. It also links processed files back + to their original reference URLs if matching '.ref' sidecar files are present. + + Args: + input_dir (str): Path to the directory containing raw source images. + output_dir (str): Path to the destination directory where WebP images will be saved. + max_size (tuple of int, optional): Maximum (width, height) bounding box for resizing + using a LANCZOS filter. Defaults to (1920, 1080). + quality (int, optional): Compression quality parameter for WebP export, + ranging from 1 to 100. Defaults to 80. + keep_original_names (bool, optional): If True, retains the original file base name + with a '.webp' extension. If False, names the file using an MD5 hash of its + optimized data stream to ensure uniqueness and prevent duplicates. Defaults to False. + + Returns: + dict: A dictionary mapping original source URLs (extracted from corresponding '.ref' + files) to the newly generated file names (e.g., {'https://example.com': 'img_abc123.webp'}). + + Raises: + No explicit exceptions are raised. Errors encountered during individual file processing + or file mutations are intercepted and printed to standard output to allow the loop to continue. + """ + if not os.path.exists(input_dir): + return {} + os.makedirs(output_dir, exist_ok=True) + valid_extensions = (".jpg", ".jpeg", ".png", ".webp") + url_to_new_path_map = {} + + for root, _, files in os.walk(input_dir): + for file in files: + if file.lower().endswith(valid_extensions): + input_path = os.path.join(root, file) + relative_path = os.path.relpath(root, input_dir) + target_folder = os.path.normpath(os.path.join(output_dir, relative_path)) + os.makedirs(target_folder, exist_ok=True) + try: + with Image.open(input_path) as img: + img = ImageOps.exif_transpose(img) + if img.mode in ("P", "CMYK"): + img = img.convert("RGBA") + img.thumbnail(max_size, Image.Resampling.LANCZOS) + buffer = BytesIO() + img.save(buffer, format="WEBP", quality=quality) + optimized_data = buffer.getvalue() + + if keep_original_names: + base_name, _ = os.path.splitext(file) + output_file_name = f"{base_name}.webp" + else: + hasher = hashlib.md5(optimized_data) + content_hash = hasher.hexdigest() + output_file_name = f"img_{content_hash}.webp" + + output_path = os.path.join(target_folder, output_file_name) + + if not os.path.exists(output_path): + with open(output_path, "wb") as f: + f.write(optimized_data) + + if os.path.exists(input_path): + os.remove(input_path) + + ref_path = os.path.join(root, f"{file}.ref") + if os.path.exists(ref_path): + with open(ref_path, "r") as ref_f: + orig_url = ref_f.read().strip() + url_to_new_path_map[orig_url] = output_file_name + + print(f"[Processed & Purged] {file} -> {output_file_name}") + except Exception as e: + print(f"Failed to process {file}. Error: {e}") + return url_to_new_path_map + +def to_kebab(text): + """ + Converts a text string into a clean, lowercased kebab-case format + suitable for slugs, URLs, or file names. + + The function strips leading/trailing whitespace, converts characters + to lowercase, removes non-alphanumeric symbols (excluding spaces + and hyphens), collapses consecutive spaces or hyphens into a single + dash, and trims leading or trailing dashes from the final output. + + Args: + text (str or None): The input text string to be slugified. + + Returns: + str: The transformed kebab-case string, or an empty string + if the input is empty or evaluates to None. + + Examples: + >>> to_kebab("Hello World!") + 'hello-world' + + >>> to_kebab(" My Awesome---Blog Post ") + 'my-awesome-blog-post' + + >>> to_kebab(None) + '' + """ + if not text: + return "" + text = text.lower().strip() + text = re.sub(r"[^a-z0-9\s-]", "", text) + return re.sub(r"[\s-]+", "-", text).strip("-") + +def main(): + """ + Executes the end-to-end extraction, asset optimization, and generation pipeline. + + This function acts as the orchestrator for the script. It loads the source JSON + payload from the environment variables, processes metadata, downloads + and compresses all remote image attachments into WebP files, and writes the final, formatted MDX + document directly to the workspace directory. + + Supported Form Input Fields (extracted from ISSUE_JSON): + title (str): The raw title text used for front matter and file naming. + description (str): A description of the new document that will be used in metadata + unit (str): unit with which to classify this document. + doc-content (str): Raw Markdown core narrative containing text and inline graphics. + + Returns: + None + + Raises: + KeyError: If the 'ISSUE_JSON' environment variable is entirely missing. + json.JSONDecodeError: If the provided configuration string contains malformed JSON data. + """ + # --- SETUP FORMS & METADATA --- + if "ISSUE_JSON" not in os.environ: + raise KeyError("Missing 'ISSUE_JSON' environment variable.") + + data = json.loads(os.environ["ISSUE_JSON"]) + + # Get safe_title and raw_title from 'title' + raw_title = data.get("title", "Untitled Document").strip() + description = data.get("description", "").strip() + selected_unit = data.get("unit", "General").strip() + doc_content = data.get("doc-content", "") + + # Clean strings for Markdown front matter + safe_title = raw_title.replace('"', '\\"') + safe_description = description.replace('"', '\\"') + + # Generate slugified file name + slug = to_kebab(raw_title) + + # --- DETERMINE TARGET DIRECTORY PATH --- + # Convert unit to kebab case for safe folder names (e.g., "troop-331") + unit_folder = to_kebab(selected_unit) + docs_directory = f"docs/{unit_folder}" + os.makedirs(docs_directory, exist_ok=True) + + # Establish asset pathways for uploaded inline document media + static_folder = f"static/img/docs/{unit_folder}/{slug}" + web_prefix = f"/img/docs/{unit_folder}/{slug}" + + # --- PARSE AND DOWNLOAD INLINE IMAGES --- + inline_matches = re.findall(r'!\[.*?\]\(((https?://[^\s\)]+))\)', doc_content) + + if inline_matches: + tmp_inline_dir = "/tmp/raw_doc_inline" + os.makedirs(tmp_inline_dir, exist_ok=True) + + for index, (full_match_url, clean_match_url) in enumerate(inline_matches): + clean_url = clean_match_url.split("?")[0] + _, ext = os.path.splitext(clean_url) + if not ext: + ext = ".png" + + inline_filename = f"image_{index}{ext}" + inline_filepath = os.path.join(tmp_inline_dir, inline_filename) + + try: + urllib.request.urlretrieve(clean_match_url, inline_filepath) + with open(f"{inline_filepath}.ref", "w") as ref_f: + ref_f.write(clean_match_url) + except Exception as e: + print(f"Failed downloading inline image {clean_match_url}: {e}") + + # Optimize downloads + inline_map = optimize_convert_and_hash_images(tmp_inline_dir, static_folder, keep_original_names=False) + + # Swap out raw URLs inside text area with local static paths + for orig_url, webp_filename in inline_map.items(): + doc_content = doc_content.replace(orig_url, f"{web_prefix}/{webp_filename}") + + # --- BUILD FRONT MATTER AND WRITING THE MARKDOWN --- + markdown_path = f"{docs_directory}/{slug}.mdx" + + # Assemble metadata block required by Docusaurus + front_matter = [ + "---", + f'title: "{safe_title}"', + f'description: "{safe_description}"', + "---", + "", + ] + + doc_payload = "\n".join(front_matter) + doc_content + + with open(markdown_path, "w", encoding="utf-8") as doc_file: + doc_file.write(doc_payload) + + print(f"[Pipeline Complete] Document successfully written to: {markdown_path}") diff --git a/.github/scripts/generate_post.py b/.github/scripts/generate_post.py index 9079140..0cd5a6d 100644 --- a/.github/scripts/generate_post.py +++ b/.github/scripts/generate_post.py @@ -179,6 +179,9 @@ def main(): json.JSONDecodeError: If the provided configuration string contains malformed JSON data. """ # --- SETUP FORMS & METADATA --- + if "ISSUE_JSON" not in os.environ: + raise KeyError("Missing 'ISSUE_JSON' environment variable.") + data = json.loads(os.environ["ISSUE_JSON"]) # Get safe_title and raw_title from 'title' diff --git a/.github/workflows/create-doc.yml b/.github/workflows/create-doc.yml new file mode 100644 index 0000000..b1a56bb --- /dev/null +++ b/.github/workflows/create-doc.yml @@ -0,0 +1,69 @@ +name: Generate Document from Issue + +on: + issues: + types: [opened] + +jobs: + create-doc: + if: contains(github.event.issue.labels.*.name, 'docs') + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@v6 + + - name: Generate GitHub App Token + id: app-token + uses: actions/create-github-app-token@v3 + with: + app-id: ${{ secrets.APP_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + + - name: Parse Issue Form + id: parse + uses: stefanbuck/github-issue-parser@v3 + with: + template-path: ./github/ISSUE_TEMPLATE/02-new-document.yml + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: '3.x' + + - name: Install Dependencies + run: | + python -m pip install --upgrade pip + pip install Pillow + + - name: Process Issue Data and Assets (Python Optimizer) + id: process + env: + ISSUE_JSON: ${{ steps.parse.outputs.jsonString }} + run: | + python .github/scripts/generate_docs.py + + - name: Create Pull Request + id: cpr + uses: peter-evans/create-pull-request@v8 + with: + token: ${{ steps.app-token.outputs.token }} + commit-message: "feat(docs): add new doc from issue #${{ github.event.issue.number }}" + branch: "automation/issue-${{ github.event.issue.number }}-${{ env.DOC_FILENAME }}" + title: "feat(docs): ${{ github.event.issue.title }}" + body: | + This Pull Request was automatically generated from Issue #${{ github.event.issue.number }}. + Closes #${{ github.event.issue.number }} + delete-branch: true + labels: | + docs + + - name: Comment on Issue with PR Link + if: steps.cpr.outputs.pull-request-number != '' + uses: peter-evans/create-or-update-comment@v5 + with: + token: ${{ steps.app-token.outputs.token }} + issue-number: ${{ github.event.issue.number }} + body: | + 🎉 Success! A staging branch has been created. + + Your document is ready for review here:👉 ${{ steps.cpr.outputs.pull-request-url }} \ No newline at end of file diff --git a/cookbook/breakfast/biscuit-gravy.mdx b/cookbook/breakfast/biscuit-gravy.mdx index a81e72b..b2e8562 100644 --- a/cookbook/breakfast/biscuit-gravy.mdx +++ b/cookbook/breakfast/biscuit-gravy.mdx @@ -1,7 +1,6 @@ --- title: Biscuits and Gravy description: Hearty meal of fresh baked biscuits and creamy sausage gravy -sidebar_position: 1 --- ## Ingredients diff --git a/cookbook/dutch-oven.mdx b/cookbook/dutch-oven.mdx index 9fafdc0..d6176b0 100644 --- a/cookbook/dutch-oven.mdx +++ b/cookbook/dutch-oven.mdx @@ -1,6 +1,5 @@ --- title: Dutch Oven Cooking -sidebar_position: 6 --- ## Why "Dutch Oven" diff --git a/cookbook/index.mdx b/cookbook/index.mdx index 2a49e09..c113d90 100644 --- a/cookbook/index.mdx +++ b/cookbook/index.mdx @@ -1,6 +1,5 @@ --- title: "Camping Recipes" -sidebar_position: 1 --- From hotdogs and s'mores over a campfire, to true gourmet delights, cooking diff --git a/docusaurus.config.js b/docusaurus.config.js index 06ccd4f..4b8dc00 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -22,6 +22,9 @@ import { themes as prismThemes } from "prism-react-renderer"; * @property {Array>} plugins - Custom multi-instance document routes and isolated post processors parsing recent content folders. * @property {import('@docusaurus/types').ThemeConfig} themeConfig - The master styling architecture setting default light-modes, banners, nav bars, and HTML social links. */ + +const SHOW_ANNOUNCEMENT = false; + const config = { title: "The Scouting Units of American Legion Post 331", tagline: @@ -122,6 +125,27 @@ const config = { }, }, ], + [ + // Capability for Cookie Consent pop-up if determined that it is necessary + // for our site. + // See https://github.com/mcclowes/docusaurus-plugin-cookie-consent for + // more configuration details + // Privacy and Cookie policy pages would need to be created. + 'docusaurus-plugin-cookie-consent', + { + title: 'Cookie Consent', + description: 'We use cookies to enhance your browsing experience and analyze our traffic.', + links: [ + { label: 'Privacy Policy', href: '/privacy' }, + { label: 'Cookie Policy', href: '/cookies' }, + ], + enabled: false, + acceptAllText: 'Accept All Cookies', + rejectOptionalText: 'Essential Only', + rejectAllText: 'Reject All', + toastMode: true, + }, + ], ], themeConfig: { @@ -131,13 +155,16 @@ const config = { disableSwitch: true, defaultMode: "light", }, - // announcementBar: { - // id: "new_website", - // content: "Welcome to our new website! Please poke around and if something could be improved, contact the webmaster.", - // backgroundColor: "var(--announcement-bar)", - // textColor: "var(--scouting-america-white)", - // isCloseable: true, - // }, + // Capability to add an announcement on the top of all pages + // Simply add text to the 'content' and change the "SHOW_ANNOUNCEMENT" + // variable above to true. + announcementBar: SHOW_ANNOUNCEMENT ? { + id: "announcement-bar", + content: 'This is an announcement', + backgroundColor: "var(--announcement-bar)", + textColor: "var(--scouting-america-white)", + isCloseable: true, + } : undefined, navbar: { title: "Scouting America", logo: { diff --git a/package-lock.json b/package-lock.json index 4d49fd7..4367dd5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "@mdx-js/react": "^3.0.0", "clsx": "^2.0.0", "csv-parse": "^6.2.1", + "docusaurus-plugin-cookie-consent": "^4.6.0", "leaflet": "^1.9.4", "prism-react-renderer": "^2.3.0", "react": "^19.0.0", @@ -11841,6 +11842,18 @@ "node": ">=6" } }, + "node_modules/docusaurus-plugin-cookie-consent": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/docusaurus-plugin-cookie-consent/-/docusaurus-plugin-cookie-consent-4.6.0.tgz", + "integrity": "sha512-eSFlW0PRODcLtrkkXujbkgt4dIGQWPsVyfIs3O09+Q1KzV7EyPVowec1vwROa7opJwYnON/yEf/czdP1D3XM/Q==", + "license": "MIT", + "peerDependencies": { + "@docusaurus/core": "^3.0.0", + "@docusaurus/types": "^3.0.0", + "react": "^18.2.0 || ^19.0.0", + "react-dom": "^18.2.0 || ^19.0.0" + } + }, "node_modules/dom-converter": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", diff --git a/package.json b/package.json index b84c8c4..ad67852 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "@mdx-js/react": "^3.0.0", "clsx": "^2.0.0", "csv-parse": "^6.2.1", + "docusaurus-plugin-cookie-consent": "^4.6.0", "leaflet": "^1.9.4", "prism-react-renderer": "^2.3.0", "react": "^19.0.0", diff --git a/src/components/BlogCard/index.jsx b/src/components/BlogCard/index.jsx index 076c6d4..9ac6029 100644 --- a/src/components/BlogCard/index.jsx +++ b/src/components/BlogCard/index.jsx @@ -78,7 +78,6 @@ function BlogCard({ permalink, title, date, authors, tags }) { - ` + `, }, ], }, ], }, - // --- CODE BLOCKS SYNTAX HIGHLIGHTING --- + // --- CODE BLOCKS SYNTAX HIGHLIGHTING --- // Controls how programming snippets look when displayed in documentation tutorials or cookbook instructions. prism: { - theme: prismThemes.github, // Uses clean light colors matching general GitHub documentation layouts. - darkTheme: prismThemes.dracula, // Fallback dark color block theme format. + theme: prismThemes.github, // Uses clean light colors matching general GitHub documentation layouts. + darkTheme: prismThemes.dracula, // Fallback dark color block theme format. }, // --- MERMAID DIAGRAM OPERATOR --- // Configures flowchart layout trees so we can build unit organization maps using text commands. @@ -336,7 +342,7 @@ const config = { { name: "keywords", content: - "scouts bsa brownsburg, cub scouts near me, brownsburg scout troops, troop 303 brownsburg, troop 331 indiana, pack 303 indiana, crew 303 ventilation, scouting america indiana, boy scouts brownsburg indiana, girl scouts bsa hendricks county, youth groups brownsburg in, kids activities brownsburg indiana, kid friendly clubs near me, youth leadership programs, eagle scout rank, cub scout advancement, kids outdoor activities hendricks county, family camping brownsburg, stem activities for kids indiana, youth community service brownsburg, child development groups, scouts bsa girls troop, cub scouts avon indiana, boy scouts pittsboro in, youth sports and adventure brownsburg, child character building programs, community youth organizations indiana", + "scouts bsa brownsburg, cub scouts near me, brownsburg scout troops, troop 303 brownsburg, troop 331 indiana, pack 303 indiana, crew 303, scouting america indiana, boy scouts brownsburg indiana, girl scouts bsa hendricks county, youth groups brownsburg in, kids activities brownsburg indiana, kid friendly clubs near me, youth leadership programs, eagle scout rank, cub scout advancement, kids outdoor activities hendricks county, family camping brownsburg, stem activities for kids indiana, youth community service brownsburg, child development groups, scouts bsa girls troop, cub scouts avon indiana, boy scouts pittsboro in, youth sports and adventure brownsburg, child character building programs, community youth organizations indiana", }, ], }, @@ -344,13 +350,13 @@ const config = { // --- MARKDOWN & PARSING ENGINES --- themes: ["@docusaurus/theme-mermaid"], // Extends theme engine capabilities to natively render Mermaid charts. markdown: { - format: "mdx", // Enforces rich MDX format so we can embed custom interactive buttons inside text files. - mermaid: true, // Turns on graph generation tools within standard markdown documents. - emoji: true, // Allows Scouts to write basic shortcuts like :tent: or :fire: to automatically show visual emojis. + format: "mdx", // Enforces rich MDX format so we can embed custom interactive buttons inside text files. + mermaid: true, // Turns on graph generation tools within standard markdown documents. + emoji: true, // Allows Scouts to write basic shortcuts like :tent: or :fire: to automatically show visual emojis. // --- SAFETY HOOKS & COMPILATION GUARDRAILS --- hooks: { - onBrokenMarkdownLinks: "warn", // 🟡 Warns us in the terminal if a text link points to an invalid section header anchor. + onBrokenMarkdownLinks: "warn", // 🟡 Warns us in the terminal if a text link points to an invalid section header anchor. onBrokenMarkdownImages: "throw", // ❌ CRASHES the local builder instantly if a Scout tries to link a photo that is missing. }, }, From 5161cbc6c51ea074ca89c975e594fb4ab2283365 Mon Sep 17 00:00:00 2001 From: shoverbj Date: Thu, 18 Jun 2026 14:26:30 -0400 Subject: [PATCH 45/50] added lots of comments --- .github/CODEOWNERS | 18 +- .github/ISSUE_TEMPLATE/01-new-blog-post.yml | 193 +++++++++++------- .github/ISSUE_TEMPLATE/02-new-document.yml | 128 +++++++----- .github/ISSUE_TEMPLATE/03-new-blog-author.yml | 87 +++++--- .github/ISSUE_TEMPLATE/04-bug_report.md | 36 ---- .github/scripts/add_author.py | 152 ++++++++------ .github/scripts/fb_router.py | 162 +++++++++------ .github/scripts/generate_docs.py | 149 ++++++++------ .github/scripts/generate_post.py | 1 + .github/workflows/blog-to-facebook.yml | 99 ++++++--- .github/workflows/content-management.yml | 159 +++++++++------ .vscode/settings.json | 1 + note.md | 130 ++++++++++++ sidebarDocs.js | 42 ++-- src/components/BlogCard/index.jsx | 69 ++++--- src/components/BlogCard/styles.module.css | 176 +++++++++++++++- src/components/Column/index.jsx | 5 +- src/components/Columns/index.jsx | 7 +- src/components/CsvTable/index.jsx | 31 ++- src/components/HeroCarousel/index.jsx | 43 ++-- src/components/HeroCarousel/index.module.css | 19 +- src/components/HomepageFeatures/index.jsx | 31 ++- .../HomepageFeatures/styles.module.css | 109 +++++++++- src/components/Map/index.jsx | 53 +++-- src/components/MeetingLocations/index.jsx | 29 ++- src/components/PhotoAlbumGallery/index.jsx | 62 +++--- src/components/RecruitmentCards/index.jsx | 66 +++--- src/components/TroopLeadership/index.jsx | 33 ++- .../TroopLeadership/styles.module.css | 99 +++++---- src/components/UpcomingEvents/index.jsx | 38 +++- src/pages/index.jsx | 27 ++- src/theme/Footer/index.jsx | 32 ++- src/theme/Logo/index.jsx | 45 ++-- 33 files changed, 1625 insertions(+), 706 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/04-bug_report.md create mode 100644 note.md diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index f4b5b90..4d1cb7a 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1,17 @@ -* @shoverbj +# ============================================================================== +# GitHub Code Owners Configuration File +# This file automatically assigns reviewers to Pull Requests based on the paths +# of the files being modified. +# +# Rules to remember: +# 1. Order matters: Later rules override earlier ones for the same file paths. +# 2. Users can be specified by @username or by an email address linked to their account. +# 3. Teams can be assigned using @org/team-name (requires GitHub Organization). +# ============================================================================== + +# ------------------------------------------------------------------------------ +# GLOBAL FALLBACK OWNERS +# ------------------------------------------------------------------------------ +# The asterisk (*) matches every file in the repository. +# These users will be requested to review a PR if no more specific rule below matches. +* @shoverbj diff --git a/.github/ISSUE_TEMPLATE/01-new-blog-post.yml b/.github/ISSUE_TEMPLATE/01-new-blog-post.yml index 3694cd7..d59a03c 100644 --- a/.github/ISSUE_TEMPLATE/01-new-blog-post.yml +++ b/.github/ISSUE_TEMPLATE/01-new-blog-post.yml @@ -1,11 +1,21 @@ -name: New Blog Post -description: "Add a new post to the blog" -labels: ["blog"] -title: "[New Blog Post]" -body: - - type: markdown - attributes: - value: | +# ============================================================================== +# GitHub Issue Form Template Configuration +# This file defines a structured form for submitting new blog posts. +# ============================================================================== + +# Core metadata for the GitHub Issue Form template +name: New Blog Post # Name of the template displayed in the GitHub UI +description: "Add a new post to the blog" # Subtext explaining the purpose of this form +labels: ["blog"] # Automatically applies this label to the created issue +title: "[New Blog Post]" # Pre-fills the issue title with this prefix +body: # Begins the list of form UI components/fields + + # ---------------------------------------------------------------------------- + # FIELD 1: Introductory Instructions (Markdown Notice) + # ---------------------------------------------------------------------------- + - type: markdown # Static text block using Markdown formatting + attributes: # Map of properties for this markdown block + value: | # Literal block scalar to preserve line breaks **The title above is just for internal use. Please don't change it** # Creating new blog post @@ -13,98 +23,131 @@ body: Fill out this form and click "Create" below to submit a new blog post. After submitting your post, it will be reviewed by the webmaster before being posted to the website. - - type: input - id: title - attributes: - label: "Blog Title" - description: | + + # ---------------------------------------------------------------------------- + # FIELD 2: Blog Post Title Input + # ---------------------------------------------------------------------------- + - type: input # Single-line text input field + id: title # Unique identifier used by automated scripts + attributes: # Map of properties for this text input + label: "Blog Title" # Heading text displayed above the text box + description: | # Subtext providing user instructions Title for your post This should include the campout/adventure/event if thats what this post is about! - placeholder: "👉 REPLACE WITH YOUR TITLE 👈" - validations: - required: true - - type: input - id: date - attributes: - label: "Post Date" - description: | + placeholder: "👉 REPLACE WITH YOUR TITLE 👈" # Temporary ghost text inside the empty box + validations: # Section for field constraints + required: true # User must fill this out to submit the form + + # ---------------------------------------------------------------------------- + # FIELD 3: Publication Date Input + # ---------------------------------------------------------------------------- + - type: input # Single-line text input field + id: date # Unique identifier for the date property + attributes: # Map of properties for this text input + label: "Post Date" # Heading text displayed above the text box + description: | # Instruction subtext Date for your blog post If this is left empty, today's date will be used. - placeholder: YYYY-MM-DD - validations: - required: false - - type: dropdown - id: authors - attributes: - label: "Authors" - description: "Select author(s) of this post" - multiple: true - options: + placeholder: YYYY-MM-DD # Example format text shown inside the empty field + validations: # Section for field constraints + required: false # Makes this an optional field for the user + + # ---------------------------------------------------------------------------- + # FIELD 4: Author Selection Dropdown + # ---------------------------------------------------------------------------- + - type: dropdown # Selectable list component + id: authors # Unique identifier for the authors list + attributes: # Map of properties for this dropdown + label: "Authors" # Heading text displayed above the dropdown + description: "Select author(s) of this post" # Instruction subtext + multiple: true # Allows users to choose more than one option + options: # List of selectable choices + # NOTE: Do not change the line below. It is used by our GitHub Action. # AUTHOR_START - Chris Koczan - Benjamin Shover # AUTHOR_END - validations: - required: true - - type: markdown - attributes: - value: | + validations: # Section for field constraints + required: true # User must choose at least one author + + # ---------------------------------------------------------------------------- + # FIELD 5: Missing Author Instructions (Markdown Link) + # ---------------------------------------------------------------------------- + - type: markdown # Static text block using Markdown formatting + attributes: # Map of properties for this markdown block + value: | # Text layout block with a link to add new authors If your name is not in the list of authors, please submit a request to add your name to the list of authors here: [New Author Request](https://github.com/scouting331/scoutSite/issues/new?template=03-new-blog-author.yml) - - type: dropdown - id: tags - attributes: - label: "Unit" - description: | + + # ---------------------------------------------------------------------------- + # FIELD 6: Unit / Tag Dropdown Selection + # ---------------------------------------------------------------------------- + - type: dropdown # Selectable list component + id: tags # Unique identifier (maps to post categories or tags) + attributes: # Map of properties for this dropdown + label: "Unit" # Heading text displayed above the dropdown + description: | # Instruction subtext Select the unit(s) to associate this post with. Multiple units may be selected. - multiple: true - options: + multiple: true # Allows selection of multiple scouting units + options: # List of available scouting units - Troop 303 - Troop 331 - Crew 303 - Pack 303 - validations: - required: true - - type: upload - id: cover-photo - attributes: - label: "Cover Photo" - description: | + validations: # Section for field constraints + required: true # User must select at least one unit + + # ---------------------------------------------------------------------------- + # FIELD 7: Cover Photo File Attachment + # ---------------------------------------------------------------------------- + - type: upload # File attachment component + id: cover-photo # Unique identifier for the cover photo asset + attributes: # Map of properties for this upload field + label: "Cover Photo" # Heading text displayed above the dropzone + description: | # Subtext containing processing warnings Photo shown at top of your blog post and on the homepage card **NOTE**: There is no way to limit the number of files in this space. Please only upload 1 photo. If multiple photos are uploaded, the script will simply choose one photo and discard the rest. - validations: - required: false - accept: ".png, .jpg, .jpeg, .gif, .svg, .webp" - - type: upload - id: photo-album - attributes: - label: "Photo Album" - description: | + validations: # Section for file upload constraints + required: false # Cover photo is not mandatory to submit + accept: ".png, .jpg, .jpeg, .gif, .svg, .webp" # Allowed image extensions + + # ---------------------------------------------------------------------------- + # FIELD 8: Photo Album File Attachment List + # ---------------------------------------------------------------------------- + - type: upload # File attachment component + id: photo-album # Unique identifier for the gallery asset batch + attributes: # Map of properties for this upload field + label: "Photo Album" # Heading text displayed above the dropzone + description: | # Subtext containing file size limits If you want to include a photo album upload all of the photos you want included here. **NOTE**: If the file is too big (>25 Mb), the photo will need to be resized before being uploaded. - validations: - required: false - accept: ".png, .jpg, .jpeg, .gif, .svg, .webp" - - type: textarea - id: blog-content - attributes: - label: "Blog Text" - description: | + validations: # Section for file upload constraints + required: false # Photo gallery is not mandatory to submit + accept: ".png, .jpg, .jpeg, .gif, .svg, .webp" # Allowed image extensions + + # ---------------------------------------------------------------------------- + # FIELD 9: Main Body Content Textarea + # ---------------------------------------------------------------------------- + - type: textarea # Large multi-line text input block + id: blog-content # Unique identifier for the markdown body text + attributes: # Map of properties for this textarea + label: "Blog Text" # Heading text displayed above the textarea + description: | # Instruction subtext Write your blog post here! - placeholder: | + placeholder: | # Boilerplate template text pre-filled inside the box ## What We Did @@ -119,9 +162,13 @@ body: the gear or recipes we used! ::: - validations: - required: true - - type: markdown - attributes: - value: | - **Please don't edit any of the other options!** + validations: # Section for field constraints + required: true # User must enter text content to submit + + # ---------------------------------------------------------------------------- + # FIELD 10: Closing Instructions (Markdown Footer) + # ---------------------------------------------------------------------------- + - type: markdown # Static text block using Markdown formatting + attributes: # Map of properties for this markdown block + value: | # Final warning text for form submitters + **Please don't edit any of the other options!** \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/02-new-document.yml b/.github/ISSUE_TEMPLATE/02-new-document.yml index 125a06b..5574f4e 100644 --- a/.github/ISSUE_TEMPLATE/02-new-document.yml +++ b/.github/ISSUE_TEMPLATE/02-new-document.yml @@ -1,11 +1,21 @@ -name: New Document -description: "Add a new document" -labels: ["docs"] -title: "[New Document]" -body: - - type: markdown - attributes: - value: | +# ============================================================================== +# GitHub Issue Form Template Configuration +# This file defines a structured form for submitting new site documentation. +# ============================================================================== + +# Core metadata for the GitHub Issue Form template +name: New Document # Name of the template displayed in the GitHub UI +description: "Add a new document" # Subtext explaining the purpose of this form +labels: ["docs"] # Automatically applies this label to the created issue +title: "[New Document]" # Pre-fills the issue title with this prefix +body: # Begins the list of form UI components/fields + + # ---------------------------------------------------------------------------- + # FIELD 1: Introductory Instructions (Markdown Notice) + # ---------------------------------------------------------------------------- + - type: markdown # Static text block using Markdown formatting + attributes: # Map of properties for this markdown block + value: | # Literal block scalar to preserve line breaks **The title above is just for internal use. Please don't change it** # Adding a new document to the site @@ -13,54 +23,74 @@ body: Fill out this form and click "Create" below to submit a new document. After submitting your document, it will be reviewed by the webmaster before being added to the website. - - type: input - id: title - attributes: - label: "Document Title" - description: | + + # ---------------------------------------------------------------------------- + # FIELD 2: Document Title Input + # ---------------------------------------------------------------------------- + - type: input # Single-line text input field + id: title # Unique identifier used by automated scripts + attributes: # Map of properties for this text input + label: "Document Title" # Heading text displayed above the text box + description: | # Subtext providing user instructions Title of the proposed document This will be displayed at the top of the document. Make sure it is descriptive of what the document is! - placeholder: "New Document" - validations: - required: true - - type: input - id: description - attributes: - label: "Document Description" - description: | + placeholder: "New Document" # Temporary ghost text inside the empty box + validations: # Section for field constraints + required: true # User must fill this out to submit the form + + # ---------------------------------------------------------------------------- + # FIELD 3: Document Description Input + # ---------------------------------------------------------------------------- + - type: input # Single-line text input field + id: description # Unique identifier for the description field + attributes: # Map of properties for this text input + label: "Document Description" # Heading text displayed above the text box + description: | # Instruction subtext Please provide a single sentence description of what this document is. - placeholder: "This is the packing list/rules/etc..." - validations: - required: true - - type: dropdown - id: tags - attributes: - label: "Unit" - description: | + placeholder: "This is the packing list/rules/etc..." # Ghost text showing an example description + validations: # Section for field constraints + required: true # User must provide a description to submit + + # ---------------------------------------------------------------------------- + # FIELD 4: Unit Selection Dropdown + # ---------------------------------------------------------------------------- + - type: dropdown # Selectable list component + id: tags # Unique identifier for the associated unit/tags + attributes: # Map of properties for this dropdown + label: "Unit" # Heading text displayed above the dropdown + description: | # Instruction subtext explaining general option Select the unit(s) to associate this post with. If this document applies to multiple units, select **General** - multiple: false - options: - - Troop 303 - - Troop 331 - - Crew 303 - - Pack 303 - - General - validations: - required: true - - type: textarea - id: doc-content - attributes: - label: "Document Text" - description: | + multiple: false # Restricts the user to choosing exactly one option + options: # List of available scouting units and categories + - Troop 303 + - Troop 331 + - Crew 303 + - Pack 303 + - General # (Fallback for cross-unit files) + validations: # Section for field constraints + required: true # User must choose a unit to submit the form + + # ---------------------------------------------------------------------------- + # FIELD 5: Main Body Content Textarea + # ---------------------------------------------------------------------------- + - type: textarea # Large multi-line text input block + id: doc-content # Unique identifier for the document content + attributes: # Map of properties for this textarea + label: "Document Text" # Heading text displayed above the textarea + description: | # Subtext noting that files/images can be attached Add the content of your document here. You can include inline images/files if needed. - placeholder: "Document text here..." - validations: - required: true - - type: markdown - attributes: - value: | + placeholder: "Document text here..." # Default ghost text inside the text editor box + validations: # Section for field constraints + required: true # User must enter text content to submit + + # ---------------------------------------------------------------------------- + # FIELD 6: Closing Instructions (Markdown Footer) + # ---------------------------------------------------------------------------- + - type: markdown # Static text block using Markdown formatting + attributes: # Map of properties for this markdown block + value: | # Final warning text for form submitters **Please don't edit any of the other options!** diff --git a/.github/ISSUE_TEMPLATE/03-new-blog-author.yml b/.github/ISSUE_TEMPLATE/03-new-blog-author.yml index 9c356d3..0666193 100644 --- a/.github/ISSUE_TEMPLATE/03-new-blog-author.yml +++ b/.github/ISSUE_TEMPLATE/03-new-blog-author.yml @@ -1,39 +1,62 @@ -name: New Blog Author -description: "Add a new author to the blog authors list" -title: "[New Author]" -labels: ["new-author"] -body: - - type: markdown - attributes: - value: | +# ============================================================================== +# GitHub Issue Form Template Configuration +# This file defines the structured submission form used by the onboarding script +# to extract new author metadata, roles, and profile avatars. +# ============================================================================== + +# Core metadata for the GitHub Issue Form template +name: New Blog Author # Name of the template displayed in the GitHub UI +description: "Add a new author to the blog authors list" # Subtext explaining the purpose of this form +title: "[New Author]" # Pre-fills the issue title with this prefix +labels: ["new-author"] # Applies the label used by the automation script to trigger runs +body: # Begins the list of form UI components/fields + + # ---------------------------------------------------------------------------- + # FIELD 1: Introductory Instructions (Markdown Notice) + # ---------------------------------------------------------------------------- + - type: markdown # Static text block using Markdown formatting + attributes: # Map of properties for this markdown block + value: | # Literal block scalar to preserve line breaks **The title above is just for internal use. Please don't change it** - - type: input - id: name - attributes: - label: Author's Name - description: | + + # ---------------------------------------------------------------------------- + # FIELD 2: Author's Name Input + # ---------------------------------------------------------------------------- + - type: input # Single-line text input field + id: name # Unique identifier read by data.get("name") in the Python script + attributes: # Map of properties for this text input + label: Author's Name # Heading text displayed above the text box + description: | # Subtext outlining important privacy rules for youth Name shown in blog posts for the author **NOTE**: If you are a Scout this should be your first name followed by a last initial, **NO LAST NAMES PLEASE** If you are an adult and want your last name shown, feel free to include it" - placeholder: "Scout A" - validations: - required: true - - type: input - id: title - attributes: - label: Title - description: "List your position(s) and unit." - placeholder: "Position, Troop ###" - validations: - required: true - - type: upload - id: image_url - attributes: - label: Avatar Image - description: | + placeholder: "Scout A" # Temporary ghost text demonstrating correct formatting + validations: # Section for field constraints + required: true # Script will exit with code 1 if this field is missing + + # ---------------------------------------------------------------------------- + # FIELD 3: Author Title / Role Input + # ---------------------------------------------------------------------------- + - type: input # Single-line text input field + id: title # Unique identifier read by data.get("title") in the Python script + attributes: # Map of properties for this text input + label: Title # Heading text displayed above the text box + description: "List your position(s) and unit." # Instructions for submission + placeholder: "Position, Troop ###" # Visual example of expected input format + validations: # Section for field constraints + required: true # Script will exit with code 1 if this field is missing + + # ---------------------------------------------------------------------------- + # FIELD 4: Avatar Image File Attachment + # ---------------------------------------------------------------------------- + - type: upload # File attachment upload component + id: image_url # Unique identifier read by data.get("image_url") in the Python script + attributes: # Map of properties for this upload field + label: Avatar Image # Heading text displayed above the dropzone + description: | # Instructions containing sizing metrics and processing alerts This is the photo that will be associated with you. Please make sure the image size is relatively small (500 x 500 pixels is ideal) and your face is centered in the image. @@ -41,7 +64,7 @@ body: **NOTE**: There is no way to limit the number of files in this space. Please only upload 1 photo. If multiple photos are uploaded, the script will simply choose one photo and discard the rest. - validations: - required: false - accept: ".png, .jpg, .jpeg, .gif, .svg, .webp" + validations: # Section for file upload constraints + required: false # Script allows missing image files and skips processing if empty + accept: ".png, .jpg, .jpeg, .gif, .svg, .webp" # Supported image extension types for conversion diff --git a/.github/ISSUE_TEMPLATE/04-bug_report.md b/.github/ISSUE_TEMPLATE/04-bug_report.md deleted file mode 100644 index 8457aa9..0000000 --- a/.github/ISSUE_TEMPLATE/04-bug_report.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve -title: "" -labels: "" -assignees: "" ---- - -**Describe the bug** A clear and concise description of what the bug is. - -**To Reproduce** Steps to reproduce the behavior: - -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error - -**Expected behavior** A clear and concise description of what you expected to -happen. - -**Screenshots** If applicable, add screenshots to help explain your problem. - -**Desktop (please complete the following information):** - -- OS: [e.g. iOS] -- Browser [e.g. chrome, safari] -- Version [e.g. 22] - -**Smartphone (please complete the following information):** - -- Device: [e.g. iPhone6] -- OS: [e.g. iOS8.1] -- Browser [e.g. stock browser, safari] -- Version [e.g. 22] - -**Additional context** Add any other context about the problem here. diff --git a/.github/scripts/add_author.py b/.github/scripts/add_author.py index baf67ad..af1a851 100644 --- a/.github/scripts/add_author.py +++ b/.github/scripts/add_author.py @@ -1,21 +1,16 @@ -""" -GitHub Action Automation Script for Author Onboarding. +# .github/scripts/add_author.py +"""GitHub Action Automation Script for Author Onboarding. This script parses author profile details from a GitHub Issue payload, validates the input fields, ensures author/slug uniqueness against an existing YAML database, downloads and converts the author's avatar to WebP format, and appends the new record to the project's central author configuration file. +It also updates the dropdown selections in the issue templates. Global Configurations: - AUTHORS_FILE (str): Path to the target YAML file where author entries are appended. - TEMPLATE_FILE (str): Path to the GitHub Issue template formatting definition. - AUTHORS_IMG_DIR (str): Destination directory for processed author avatars. - -Environment Dependencies: - ISSUE_DATA: A JSON string passed via GitHub Actions runner containing: - - name (str): The author's full name. - - title (str): The author's professional role or title. - - image_url (str): Markdown-wrapped URL pointing to the user's avatar. + AUTHORS_FILE (str): Path to the target author YAML registry database. + TEMPLATE_FILE (str): Path to the GitHub Issue form definition template. + AUTHORS_IMG_DIR (str): Destination directory for optimized author avatars. """ import json @@ -26,117 +21,156 @@ import urllib.request from PIL import Image, ImageOps -# Constants defining project directory structure +# Constant definitions for project directories and structural files AUTHORS_FILE = 'blog/authors.yml' -TEMPLATE_FILE = '.github/ISSUE_TEMPLATE/01-new-blog-post.yml' +TEMPLATE_FILE = '.github/ISSUE_TEMPLATE/new-blog-post.yml' AUTHORS_IMG_DIR = 'static/img/blog/authors' def main(): - """ - Processes and onboard a new blog author from GitHub Actions issue data. + """Process and onboard a new blog author from GitHub Actions issue data. This function extracts author metadata from an environment-supplied JSON - string, performs validation, generates a URL-safe unique slug, downloads - the remote avatar, converts it to WebP format, and appends the finalized - profile data to the project's authors YAML registry. + string, performs structural validation, generates a URL-safe unique slug, + downloads the remote avatar, converts it to WebP format, and appends the + finalized profile data to the project's authors registry document. It also + regenerates and sorts the author selection options inside the issue forms. Raises: - SystemExit (1): If required inputs are missing, or if the author's + SystemExit (1): If required fields are missing, or if the author's name already exists in the registry database. """ - - # Retrieve issue data passed as a JSON string from the CI/CD environment + # Retrieve issue metadata passed as a JSON string from the GitHub Actions runner issue_json = os.environ.get("ISSUE_DATA", "{}") data = json.loads(issue_json) - # Extract and clean author input fields + # Extract and normalize user string input data author_name = data.get("name", "").strip() author_title = data.get("title", "").strip() raw_image_url = data.get("image_url", "").strip() - # Isolate the image URL if it's wrapped in markdown format e.g. (https://url.com) + # Isolate image URL using regex if it is wrapped inside Markdown syntax e.g., (https://url.com) image_url = "" url_match = re.search(r'\((https://[^\)]+)\)', raw_image_url) if url_match: image_url = url_match.group(1) - # Halt execution if required fields are missing + # Terminate workflow execution if critical metadata is missing if not author_name or not author_title: print("Missing required fields. Exiting.") sys.exit(1) - # Read existing content to check for duplicates without rewriting via YAML loader + # Read the existing document text to safely scan for duplicate profiles raw_content = "" if os.path.exists(AUTHORS_FILE): with open(AUTHORS_FILE, 'r', encoding='utf-8') as f: raw_content = f.read() - # Simple case-insensitive duplicate name check in existing YAML + # Guard clause preventing duplicate submissions of existing author names if f"name: {author_name}" in raw_content or f'name: "{author_name}"' in raw_content: print(f"::error::The author name '{author_name}' already exists.") sys.exit(1) - # Generate a standard URL-friendly slug from the author's name + # Initialize URL-safe slug creation by normalizing to lowercase alphanumeric characters slug = author_name.lower() slug = re.sub(r'[^a-z0-9\s-]', '', slug) slug = re.sub(r'[\s-]+', '-', slug).strip('-') - + final_slug = slug counter = 1 - - # Scan file text to ensure slug uniqueness, appending a counter if it collides + + # Loop and append incremental numerical suffixes if slug collisions exist while f"{final_slug}:" in raw_content: final_slug = f"{slug}-{counter}" counter += 1 - # Process and download the avatar image if an URL was provided + # Process and optimize image resources if a valid link was detected final_image_path = "" if image_url: os.makedirs(AUTHORS_IMG_DIR, exist_ok=True) - # Remove any query parameters from URL to get file extension + # Discard tracking or token query strings from URL to isolate extension clean_url = image_url.split("?") _, ext = os.path.splitext(clean_url[0]) if not ext: - ext = ".jpg" + ext = ".jpg" # Fall back to JPG extension if undetected - # Download image to a temporary file path + # Stash download payload in server /tmp space tmp_avatar_path = f"/tmp/raw_avatar{ext}" try: urllib.request.urlretrieve(image_url, tmp_avatar_path) - # Format and convert the image to WebP using Pillow + # Setup image configuration names and location paths target_file_name = f"{final_slug}.webp" target_full_path = os.path.join(AUTHORS_IMG_DIR, target_file_name) - # Open the temporary image, convert, and save as webp + # Execute image processing pipeline via Pillow (PIL) with Image.open(tmp_avatar_path) as img: - img = ImageOps.exif_transpose(img) # Preserve original orientation - img.convert("RGB").save(target_full_path, "webp", quality=80) - - final_image_path = target_full_path - + img = ImageOps.exif_transpose(img) # Re-orient image according to metadata tags + if img.mode in ("P", "CMYK"): # Convert non-standard modes to preserve transparencies + img = img.convert("RGBA") + img.thumbnail((500, 500), Image.Resampling.LANCZOS) # High-fidelity scale reduction + img.save(target_full_path, format="WEBP", quality=85) # Save and optimize file space + + # Save the final relative public path to be referenced on the blog front-end + final_image_path = f"/img/blog/authors/{target_file_name}" + print(f"Successfully processed and saved avatar to {target_full_path}") + + # Clean up server system storage by removing the raw downloaded asset + if os.path.exists(tmp_avatar_path): + os.remove(tmp_avatar_path) except Exception as e: - print(f"Warning: Could not process image from {image_url}. Error: {e}") - # Fallback if image processing fails - final_image_path = "" - - # Prepare author object for YAML - new_author_entry = { - final_slug: { - "name": author_name, - "title": author_title, - "image": final_image_path if final_image_path else None - } - } - - # Append the new author YAML block to the end of the authors file - with open(AUTHORS_FILE, 'a', encoding='utf-8') as f: - yaml.dump(new_author_entry, f, default_flow_style=False, allow_unicode=True) + print(f"Warning: Failed to download or process avatar image. Error: {e}") + + # Build the textual YAML data mapping block to safely maintain standard docstrings + entry_lines = [ + f"{final_slug}:", + f" name: {author_name}", + f" title: {author_title}", + " page: true" + ] + if final_image_path: + entry_lines.append(f" image_url: {final_image_path}") + + raw_append_block = "\n" + "\n".join(entry_lines) + "\n" - print(f"Successfully added author '{author_name}' with slug '{final_slug}'.") + # Append structural string configurations to the bottom of the files document map + with open(AUTHORS_FILE, 'a', encoding='utf-8') as f: + f.write(raw_append_block) + print(f"Successfully appended new profile block to {AUTHORS_FILE}") + # --- PART 2: REGEX SEARCH EXTRAC NAME ATTRIBUTES TO BUILD DROPDOWNS --- + with open(AUTHORS_FILE, 'r', encoding='utf-8') as f: + updated_raw_content = f.read() + + # Isolate individual text strings matching metadata properties + all_names = re.findall(r'^\s*name:\s*["\']?(.*?)["\']?\s*$', updated_raw_content, re.MULTILINE) + all_names = [n.strip() for n in all_names if n.strip()] + all_names.sort() # Arrange elements in alphanumeric order + + # Format items to valid list elements matching template indent spaces + yaml_lines = [f" - {name}" for name in all_names] + replacement_string = "\n".join(yaml_lines) + + # Perform regular expression lookup and insert the list inside the anchor tags + if os.path.exists(TEMPLATE_FILE): + with open(TEMPLATE_FILE, 'r') as f: + template_content = f.read() + + # Regex anchor block logic tracking target replacement parameters + pattern = r'(# AUTHOR_START\n)(.*?)(\n\s*# AUTHOR_END)' + updated_content = re.sub( + pattern, + f"\\1{replacement_string}\\3", + template_content, + flags=re.DOTALL + ) + + # Commit updated structured listings back to local document storage files + with open(TEMPLATE_FILE, 'w') as f: + f.write(updated_content) + print(f"Successfully updated dropdown in {TEMPLATE_FILE}") + else: + print(f"Warning: Template file {TEMPLATE_FILE} not found. Skipping dropdown injection.") if __name__ == "__main__": main() - diff --git a/.github/scripts/fb_router.py b/.github/scripts/fb_router.py index 0ca1577..7321c23 100644 --- a/.github/scripts/fb_router.py +++ b/.github/scripts/fb_router.py @@ -1,135 +1,171 @@ +# .github/scripts/fb_router.py """Facebook Routing Automation for Docusaurus Blogs. This script parses recently added Docusaurus Markdown files, extracts their -metadata (title, slug, tags), and determines the target Facebook Page -destination based on defined tagging rules. The values are then exported -to the GitHub Actions environment. +metadata (title, slug, tags), and maps them against official scouting unit tag +taxonomies (`troop-303`, `troop-331`, `crew-303`, `pack-303`). It supports +multi-page routing by generating a compact JSON matrix array of all matched +target platforms, which is then streamed directly into the GitHub Actions runner. + +Global Configurations: + cmd (list): Set of base terminal commands used to poll the native Git tree history. + github_output_path (str): Pointer destination used to write runner outputs. """ import os import re import subprocess import sys +import json def get_last_commit_added_blog_file(): """Finds the first newly added markdown file in the blog directory. - Queries git diff for files added in the most recent commit and filters - them to ensure they reside in the Docusaurus 'blog/' folder and have - a markdown extension. + Queries the local git repository history log for files added in the most + recent commit and filters them to guarantee they reside in the Docusaurus + 'blog/' subdirectory path and possess an authorized markdown extension. Returns: - list[str] | None: A list containing file paths of new blog posts, + str | None: The relative string file path of the discovered blog post, or None if no matching files are found or an error occurs. """ try: - # Run git diff command to locate new files + # Configures Git flag array: isolates newly added files ('A') in the last commit (HEAD~1 to HEAD) cmd = ["git", "diff", "--name-only", "--diff-filter=A", "HEAD~1", "HEAD"] + + # Executes the Git process shell safely, capturing stdout text streams inside our runtime environment result = subprocess.run(cmd, capture_output=True, text=True, check=True) - files = result.stdout.splitlines() + files = result.stdout.splitlines() # Splits raw multi-line terminal block into clean array lists - # Filter for markdown files in the blog folder + # Evaluates found assets, ensuring they live in the blog folder and terminate with valid markdown extensions blog_files = [f for f in files if f.startswith("blog/") and f.endswith((".md", ".mdx"))] - return blog_files[0] if blog_files else None + return blog_files[0] if blog_files else None # Returns the earliest discovered item path string or None except subprocess.CalledProcessError: - print("Error reading git diff.") + print("Error reading git diff.") # Log alert notifying developers to shell system errors return None def parse_front_matter(file_path): """Extracts title, slug, and tags from Docusaurus front matter. - Reads the top YAML block of the markdown file using regular expressions. - If a slug is missing, it automatically derives one from the filename - while stripping out standard Docusaurus date prefixes. + Reads the raw text of a markdown document using regular expressions to isolate + metadata properties. If a custom slug is omitted, it extracts the date components + (YYYY, MM, DD) and the trailing text title out of the file name to build a + standardized permalink array layout. Args: - file_path (str): The relative path to the markdown file. + file_path (str): The relative path targeting the markdown file. Returns: - tuple[str, str, str]: A tuple containing the post title, the URL slug, - and a lowercase block of tag strings for categorization. + tuple[str, str, str]: A tuple containing the extracted post title string, + the normalized relative path slug, and a cleaned block of tags text. """ + # Safe open stream using standard global UTF-8 encoding rules to prevent character corruption with open(file_path, "r", encoding="utf-8") as f: content = f.read() - # Match the front matter block enclosed between --- + # Regex mapping logic hunting down front-matter metadata maps bounded between '---' markers front_matter_match = re.search(r"^---\s*\n(.*?)\n---", content, re.DOTALL | re.MULTILINE) if not front_matter_match: - return None, None, "" + return None, None, "" # Graceful exit fallback routing sequence if front-matter is missing - fm_text = front_matter_match.group(1) + fm_text = front_matter_match.group(1) # Isolates internal metadata configuration block - # Extract title - title_match = re.search(r"^title:\s*(.*)$", fm_text, re.MULTILINE) + # Extracts post title strings, matching text right of the identifier key prefix + title_match = re.search(r"^title:\s*(.*)\$", fm_text, re.MULTILINE) title = title_match.group(1).strip(" '\"") if title_match else "New Blog Post" - # Extract slug - slug_match = re.search(r"^slug:\s*(.*)$", fm_text, re.MULTILINE) + # Extracts permalink slug parameter, matching text right of the identifier key prefix + slug_match = re.search(r"^slug:\s*(.*)\$", fm_text, re.MULTILINE) if slug_match: slug = slug_match.group(1).strip(" '\"") + # Enforces a unified standard starting forward slash layout parameter if missing from custom slug + if not slug.startswith("/"): + slug = f"/{slug}" else: - # Fallback to filename tracking if slug is omitted - filename = os.path.basename(file_path) - base_name = os.path.splitext(filename)[0] - slug = re.sub(r"^\d{4}-\d{2}-\d{2}-", "", base_name) # Strip date prefix - - # Extract tags section as a lowercase block for easier routing matching - tags_match = re.search(r"^tags:\s*(.*)$", fm_text, re.MULTILINE) + # Fallback to structural filename directory extraction if the slug key is omitted entirely + filename = os.path.basename(file_path) # Extracts the base filename context out of the path directory tree + base_name = os.path.splitext(filename)[0] # Separates base file text characters from trailing extensions + + # Regex to break "YYYY-MM-DD-filename" patterns into individual clean URL subdirectory fragments + date_match = re.match(r"^(\d{4})-(\d{2})-(\d{2})-(.*)\$", base_name) + if date_match: + year, month, day, clean_title = date_match.groups() + slug = f"/{year}/{month}/{day}/{clean_title}" # Stitches date tokens into permalink formats: /YYYY/MM/DD/title + else: + slug = f"/{base_name}" # Simple catch-all filename routing configuration format if dates are missing + + # Extracts raw tags line string parameter, converting entries to lower case for reliable string checking + tags_match = re.search(r"^tags:\s*(.*)\$", fm_text, re.MULTILINE) tags_text = tags_match.group(1).lower() if tags_match else "" - # Handle multi-line YAML lists for tags if single line wasn't caught cleanly + # Secondary array scanner block if tags are formatted across multi-line lists instead of single brackets if not tags_text and "tags:" in fm_text: - tags_text = fm_text.split("tags:")[1].lower() + tags_text = fm_text.split("tags:")[1].lower() # Isolates downstream array characters for processing return title, slug, tags_text -def determine_target_page(tags_text): - """Maps tags to specific Facebook page categories. +def determine_target_pages(tags_text): + """Maps markdown tags to all matching scouting unit Facebook page categories. - Evaluates the parsed tags string against keyword lists to find matches - for specialized Facebook Pages (e.g., TECH or LIFESTYLE). Falls back to - a DEFAULT profile if no conditions match. + Scans the cleaned tag text string data blocks for occurrences of official + scouting unit key tokens (`troop-331`, `pack-303`, `crew-303`, `troop-303`). + Allows concurrent publishing configurations by mapping multiple items. Args: tags_text (str): A string containing the post's front-matter tags. Returns: - str: A string token ("TECH", "LIFESTYLE", or "DEFAULT") indicating - the designated target channel. - """ - tech_keywords = ["tech", "programming", "coding", "developer"] - lifestyle_keywords = ["lifestyle", "travel", "personal"] - - if any(kw in tags_text for kw in tech_keywords): - return "TECH" - elif any(kw in tags_text for kw in lifestyle_keywords): - return "LIFESTYLE" - return "DEFAULT" + list[str]: An array of uppercase matrix tokens (e.g., ["TROOP_303", "PACK_303"]) + specifying every intended social page destination channel. + ```""" + # Strips away formatting artifacts like quotes and brackets from raw array blocks + tags_clean = tags_text.replace("[", "").replace("]", "").replace('"', '').replace("'", "") + targets = [] # Allocation container list tracking targets + + # Independent conditional evaluation sequences enabling parallel multi-tag assignment mapping rules + if "troop-331" in tags_clean: + targets.append("TROOP_331") + if "pack-303" in tags_clean: + targets.append("PACK_303") + if "crew-303" in tags_clean: + targets.append("CREW_303") + if "troop-303" in tags_clean: + targets.append("TROOP_303") + + # Catch-all destination classification assignment bucket if no explicit unit tag is found + if not targets: + targets.append("DEFAULT") + + return targets def main(): """Orchestrates script lifecycle execution. - Coordinates the discovery, extraction, categorization, and exporting - of blog metadata to the `GITHUB_OUTPUT` file stream for consumption by - downstream GitHub workflow pipeline steps. + Coordinates the discovery, file text extraction, matrix categorization, and + exporting of blog post properties directly to the `GITHUB_OUTPUT` file stream + destination path for usage by downstream GitHub workspace workflow runners. """ - new_file = get_last_commit_added_blog_file() + new_file = get_last_commit_added_blog_file() # Polls git repository tree data for newest content logs if not new_file: print("No new blog files found.") + # Tells the main workflow that an article was not found, initializing safe shutdown variables with open(os.environ["GITHUB_OUTPUT"], "a") as go: go.write("has_new_post=false\n") - sys.exit(0) + go.write("targets=[]\n") + sys.exit(0) # Regular clean operational termination sequence code + # Executes data transformation pipelines against the discovered markdown asset title, slug, tags_text = parse_front_matter(new_file) - target_page = determine_target_page(tags_text) + targets = determine_target_pages(tags_text) - # Expose variables to subsequent GitHub Actions steps via GITHUB_OUTPUT - github_output_path = os.environ["GITHUB_OUTPUT"] + # Expose metadata variables to subsequent GitHub Actions runner steps via the GITHUB_OUTPUT environment path + github_output_path = os.environ["GITHUB_OUTPUT"] # Locates dynamic engine runner variable tracking path link with open(github_output_path, "a") as go: - go.write(f"has_new_post=true\n") - go.write(f"title={title}\n") - go.write(f"slug={slug}\n") - go.write(f"target_page={target_page}\n") + go.write(f"has_new_post=true\n") # Tells the workflow step gate that data exists to process + go.write(f"title={title}\n") # Streams the post heading to populate status text bodies + go.write(f"slug={slug}\n") # Streams the relative link string array for absolute url rendering + # Dumps the Python array into a minified, stringified JSON array block string for GitHub Actions Matrix consumption + go.write(f"targets={json.dumps(targets)}\n") if __name__ == "__main__": - main() + main() # Executes the application runtime lifecycle routine diff --git a/.github/scripts/generate_docs.py b/.github/scripts/generate_docs.py index aacc929..3559ffe 100644 --- a/.github/scripts/generate_docs.py +++ b/.github/scripts/generate_docs.py @@ -1,5 +1,5 @@ -""" -Docusaurus Document Generator and Asset Optimizer. +# .github/scripts/generate_docs.py +"""Docusaurus Document Generator and Asset Optimizer. This module automates the generation of MDX documents for Docusaurus from structured GitHub Issue Form JSON data. It extracts front matter details @@ -27,14 +27,14 @@ import json import re import hashlib -import urllib.request +import urllib.request from io import BytesIO from PIL import Image, ImageOps # --- Helper functions --- def optimize_convert_and_hash_images(input_dir, output_dir, max_size=(1920, 1080), quality=80, keep_original_names=False): - """ - Optimizes, resizes, and converts images in an input directory to WebP format, + """Optimizes, resizes, and converts images in an input directory to WebP format, + saving them to an output directory while optionally renaming them using MD5 hashes. This function traverses an input folder structure, processes supported images @@ -62,60 +62,70 @@ def optimize_convert_and_hash_images(input_dir, output_dir, max_size=(1920, 1080 No explicit exceptions are raised. Errors encountered during individual file processing or file mutations are intercepted and printed to standard output to allow the loop to continue. """ + # Guard clause to skip image optimization processing runs if the source directory does not exist if not os.path.exists(input_dir): return {} + + # Safely creates the target destination output directory tree path mapping layers os.makedirs(output_dir, exist_ok=True) - valid_extensions = (".jpg", ".jpeg", ".png", ".webp") - url_to_new_path_map = {} + valid_extensions = (".jpg", ".jpeg", ".png", ".webp") # Tuple listing supported source assets to process + url_to_new_path_map = {} # Allocation map container linking original URLs to new filenames + # Recursively step down through the local raw file directory storage tree structures for root, _, files in os.walk(input_dir): for file in files: + # Check if the file extension matches our list of target images if file.lower().endswith(valid_extensions): - input_path = os.path.join(root, file) - relative_path = os.path.relpath(root, input_dir) - target_folder = os.path.normpath(os.path.join(output_dir, relative_path)) - os.makedirs(target_folder, exist_ok=True) + input_path = os.path.join(root, file) # Constructs the absolute system source path to the file + relative_path = os.path.relpath(root, input_dir) # Tracks internal subfolder offsets relative to root + target_folder = os.path.normpath(os.path.join(output_dir, relative_path)) # Formulates output directory + os.makedirs(target_folder, exist_ok=True) # Creates target subfolders if missing on disk try: + # Instantiates a clean PIL workspace buffer tracking the source image file with Image.open(input_path) as img: - img = ImageOps.exif_transpose(img) - if img.mode in ("P", "CMYK"): + img = ImageOps.exif_transpose(img) # Re-orients image automatically using structural EXIF metadata tags + if img.mode in ("P", "CMYK"): # Normalizes palette and print profiles to preserve alpha channels img = img.convert("RGBA") - img.thumbnail(max_size, Image.Resampling.LANCZOS) - buffer = BytesIO() - img.save(buffer, format="WEBP", quality=quality) - optimized_data = buffer.getvalue() + img.thumbnail(max_size, Image.Resampling.LANCZOS) # High-fidelity scale reduction utilizing Lanczos algorithm + buffer = BytesIO() # Initializes an in-memory byte pipeline stream buffer + img.save(buffer, format="WEBP", quality=quality) # Compresses and writes file into memory as WebP format + optimized_data = buffer.getvalue() # Extracts the optimized binary byte layout mapping data payload + # Evaluates naming configurations to resolve output string file structures if keep_original_names: - base_name, _ = os.path.splitext(file) + base_name, _ = os.path.splitext(file) # Separates filename strings from extensions output_file_name = f"{base_name}.webp" else: - hasher = hashlib.md5(optimized_data) - content_hash = hasher.hexdigest() + hasher = hashlib.md5(optimized_data) # Instantiates MD5 engine against the unique file bits stream + content_hash = hasher.hexdigest() # Generates unique hex hash identifier mapping key output_file_name = f"img_{content_hash}.webp" - output_path = os.path.join(target_folder, output_file_name) + output_path = os.path.join(target_folder, output_file_name) # Builds output file destination block path + # Guard clause checking local asset directories to ensure duplicate files are not overwritten if not os.path.exists(output_path): with open(output_path, "wb") as f: - f.write(optimized_data) + f.write(optimized_data) # Commits byte layout data safely to permanent local storage disk + # Delete the original raw file from temporary disk to preserve server space allocations if os.path.exists(input_path): os.remove(input_path) + # Scan for `.ref` sidecar metadata maps containing the original source download URL address link ref_path = os.path.join(root, f"{file}.ref") if os.path.exists(ref_path): with open(ref_path, "r") as ref_f: - orig_url = ref_f.read().strip() - url_to_new_path_map[orig_url] = output_file_name + orig_url = ref_f.read().strip() # Extracts original URL address string + url_to_new_path_map[orig_url] = output_file_name # Updates internal tracking links dictionary maps print(f"[Processed & Purged] {file} -> {output_file_name}") except Exception as e: - print(f"Failed to process {file}. Error: {e}") - return url_to_new_path_map + print(f"Failed to process {file}. Error: {e}") # Log error messages to prevent entire automation pipeline crashes + return url_to_new_path_map # Returns the collection linking old asset links to new files def to_kebab(text): - """ - Converts a text string into a clean, lowercased kebab-case format + """Converts a text string into a clean, lowercased kebab-case format + suitable for slugs, URLs, or file names. The function strips leading/trailing whitespace, converts characters @@ -141,14 +151,13 @@ def to_kebab(text): '' """ if not text: - return "" - text = text.lower().strip() - text = re.sub(r"[^a-z0-9\s-]", "", text) - return re.sub(r"[\s-]+", "-", text).strip("-") + return "" # Edge case mitigation handling missing properties gracefully + text = text.lower().strip() # Normalizes character cases and clips exterior whitespace frames + text = re.sub(r"[^a-z0-9\s-]", "", text) # Regex scrubbing punctuation and non-alphanumeric configuration patterns + return re.sub(r"[\s-]+", "-", text).strip("-") # Condenses multi-spaces into single hyphens and trims loose edges def main(): - """ - Executes the end-to-end extraction, asset optimization, and generation pipeline. + """Executes the end-to-end extraction, asset optimization, and generation pipeline. This function acts as the orchestrator for the script. It loads the source JSON payload from the environment variables, processes metadata, downloads @@ -160,77 +169,88 @@ def main(): description (str): A description of the new document that will be used in metadata unit (str): unit with which to classify this document. doc-content (str): Raw Markdown core narrative containing text and inline graphics. - - Returns: + Returns: None - - Raises: + Raises: KeyError: If the 'ISSUE_JSON' environment variable is entirely missing. json.JSONDecodeError: If the provided configuration string contains malformed JSON data. - """ + """ + # --- SETUP FORMS & METADATA --- + # Guard clause asserting that the required input variable stream data block is active on the server if "ISSUE_JSON" not in os.environ: raise KeyError("Missing 'ISSUE_JSON' environment variable.") - - data = json.loads(os.environ["ISSUE_JSON"]) + # Decodes incoming issue string parameters into interactive structural python dictionaries + data = json.loads(os.environ["ISSUE_JSON"]) + # Get safe_title and raw_title from 'title' - raw_title = data.get("title", "Untitled Document").strip() - description = data.get("description", "").strip() - selected_unit = data.get("unit", "General").strip() - doc_content = data.get("doc-content", "") + raw_title = data.get("title", "Untitled Document").strip() # Pulls the document heading text from form mappings + description = data.get("description", "").strip() # Pulls the descriptive subtext metadata parameter string + + # Extract structural form input parameters from the loaded configuration map + selected_unit = data.get("unit", "General").strip() # Fetches target sorting classification group + doc_content = data.get("doc-content", "") # Extracts core multi-line markdown narrative layout text - # Clean strings for Markdown front matter + # Escape interior quote marks within text strings to prevent formatting breaks inside front matter metadata safe_title = raw_title.replace('"', '\\"') safe_description = description.replace('"', '\\"') - # Generate slugified file name + # Transform raw title characters into a web-safe, standardized lower-case kebab-case text slug slug = to_kebab(raw_title) # --- DETERMINE TARGET DIRECTORY PATH --- - # Convert unit to kebab case for safe folder names (e.g., "troop-331") + # Convert chosen classification category name to a clean directory path format (e.g., "troop-331") unit_folder = to_kebab(selected_unit) - docs_directory = f"docs/{unit_folder}" - os.makedirs(docs_directory, exist_ok=True) + docs_directory = f"docs/{unit_folder}" # Assembles path to the native Docusaurus document content tree + os.makedirs(docs_directory, exist_ok=True) # Provisions structural folder pathways on disk if missing - # Establish asset pathways for uploaded inline document media - static_folder = f"static/img/docs/{unit_folder}/{slug}" - web_prefix = f"/img/docs/{unit_folder}/{slug}" + # Set up static asset pipeline storage targets and frontend reference url strings + static_folder = f"static/img/docs/{unit_folder}/{slug}" # Permanent hard disk destination directory path + web_prefix = f"/img/docs/{unit_folder}/{slug}" # Client-side routing reference path used in generated web pages # --- PARSE AND DOWNLOAD INLINE IMAGES --- + # Regular expression capturing markdown image link variants, isolating raw matching pairs and clean URLs inline_matches = re.findall(r'!\[.*?\]\(((https?://[^\s\)]+))\)', doc_content) + # Conditional workflow block executing download pipelines if inline graphic links were identified if inline_matches: - tmp_inline_dir = "/tmp/raw_doc_inline" - os.makedirs(tmp_inline_dir, exist_ok=True) + tmp_inline_dir = "/tmp/raw_doc_inline" # Defines isolated local runner workspace scratchpad buffer path + os.makedirs(tmp_inline_dir, exist_ok=True) # Safely constructs server workspace environment layer + # Iterates through every unique index instance matching inline url string criteria for index, (full_match_url, clean_match_url) in enumerate(inline_matches): - clean_url = clean_match_url.split("?")[0] - _, ext = os.path.splitext(clean_url) + clean_url = clean_match_url.split("?")[0] # Discards query string parameters to protect clean path matching + _, ext = os.path.splitext(clean_url) # Extracts file extension out of the isolated tracking string if not ext: - ext = ".png" + ext = ".png" # Default extension fallback parameter if undetected + # Standardizes raw local filename text formats using iteration trackers inline_filename = f"image_{index}{ext}" - inline_filepath = os.path.join(tmp_inline_dir, inline_filename) + inline_filepath = os.path.join(tmp_inline_dir, inline_filename) # Builds absolute path context link try: + # Dispatches server download request stream fetching remote assets directly onto the local drive urllib.request.urlretrieve(clean_match_url, inline_filepath) + # Writes `.ref` sidecar files containing original absolute URL references to assist optimization mapping steps with open(f"{inline_filepath}.ref", "w") as ref_f: ref_f.write(clean_match_url) except Exception as e: - print(f"Failed downloading inline image {clean_match_url}: {e}") + print(f"Failed downloading inline image {clean_match_url}: {e}") # Non-blocking diagnostic tracker logging - # Optimize downloads + # Optimize downloads: scales, hashes, saves optimized assets, and removes temporary items from scratch space inline_map = optimize_convert_and_hash_images(tmp_inline_dir, static_folder, keep_original_names=False) # Swap out raw URLs inside text area with local static paths + # Step down through calculated mapping dictionary records to update text body markup variables for orig_url, webp_filename in inline_map.items(): doc_content = doc_content.replace(orig_url, f"{web_prefix}/{webp_filename}") # --- BUILD FRONT MATTER AND WRITING THE MARKDOWN --- - markdown_path = f"{docs_directory}/{slug}.mdx" + markdown_path = f"{docs_directory}/{slug}.mdx" # Defines ultimate destination path for the document # Assemble metadata block required by Docusaurus + # Constructs a list array mapping standard key-value headers to fulfill Front Matter criteria front_matter = [ "---", f'title: "{safe_title}"', @@ -239,12 +259,15 @@ def main(): "", ] + # Merges compiled header metadata lists with modified markdown text blocks via joining newline strings doc_payload = "\n".join(front_matter) + doc_content + # Writes operational file contents back down to permanent project storage with universal UTF-8 file layouts with open(markdown_path, "w", encoding="utf-8") as doc_file: doc_file.write(doc_payload) print(f"[Pipeline Complete] Document successfully written to: {markdown_path}") if __name__ == "__main__": - main() \ No newline at end of file + main() # Executes the core application script runtime routine + diff --git a/.github/scripts/generate_post.py b/.github/scripts/generate_post.py index 20adaea..6e0e89d 100644 --- a/.github/scripts/generate_post.py +++ b/.github/scripts/generate_post.py @@ -1,3 +1,4 @@ +# .github/scripts/generate_post.py """ Docusaurus Blog Post Generator and Asset Optimizer. diff --git a/.github/workflows/blog-to-facebook.yml b/.github/workflows/blog-to-facebook.yml index df1bc01..d8d1cd0 100644 --- a/.github/workflows/blog-to-facebook.yml +++ b/.github/workflows/blog-to-facebook.yml @@ -1,55 +1,92 @@ -name: Route Blog Posts to Facebook Pages +# ============================================================================== +# GitHub Actions Automation Workflow: Multi-Target Blog Post Router to Facebook +# Automatically triggers when code commits are pushed into main containing blog modifications. +# Leverages an external Python tracking script to build a dynamic target matrix array, +# then spawns parallel posting steps targeting Troop 303, Troop 331, Pack 303, or Crew 303 pages. +# ============================================================================== -on: - push: +name: Route Blog Posts to Facebook Pages # Friendly workflow tracking name shown in Actions panel + +on: # Defines the system event trigger hooks + push: # Listens to push event blocks across the ecosystem branches: - - main + - main # Restricts execution to the master production code trunk line branch paths: - - 'blog/**' + - 'blog/**' # Limits trigger conditions to file modifications inside the blog directory tree -jobs: - facebook-routing: - runs-on: ubuntu-latest +jobs: # Begins structural job step pipeline workflows + # ---------------------------------------------------------------------------- + # JOB 1: Discover modification targets and serialize metadata values + # ---------------------------------------------------------------------------- + parse-blog: + runs-on: ubuntu-latest # Provisions a fresh virtual machine Linux runner workspace environment + outputs: # Maps global outputs from local tracking steps to make them accessible across jobs + has_new_post: ${{ steps.blog_data.outputs.has_new_post }} + title: ${{ steps.blog_data.outputs.title }} + slug: ${{ steps.blog_data.outputs.slug }} + targets: ${{ steps.blog_data.outputs.targets }} # Contains the stringified JSON target array e.g., ["TROOP_303", "PACK_303"] steps: - - name: Checkout repository + - name: Checkout repository # Step 1: Fetches repository source documentation onto the runner platform uses: actions/checkout@v4 with: - fetch-depth: 2 + fetch-depth: 2 # Forces checking history depths of 2 commits so git diff can track HEAD~1 shifts - - name: Set up Python + - name: Set up Python # Step 2: provisions Python interpreter infrastructure binaries uses: actions/setup-python@v5 with: - python-version: '3.x' + python-version: '3.x' # Always downloads the latest functional patch build iteration of Python 3 - - name: Parse Metadata via Python - id: blog_data + - name: Parse Metadata via Python # Step 3: Run router script to identify targeting parameters + id: blog_data # Step identifier token used by outputs map fields to find variables run: python .github/scripts/fb_router.py - - name: Set Facebook Credentials Dynamically - if: steps.blog_data.outputs.has_new_post == 'true' - id: set_tokens + # ---------------------------------------------------------------------------- + # JOB 2: Run dynamic multi-process loops to dispatch entries onto Facebook Pages + # ---------------------------------------------------------------------------- + facebook-routing: + needs: parse-blog # Inter-job dependency lock: halts execution until data collection completes + # Execution gate: only runs if a post was successfully identified and targets list array contains entries + if: needs.parse-blog.outputs.has_new_post == 'true' && needs.parse-blog.outputs.targets != '[]' + runs-on: ubuntu-latest # Provisions an independent parallel system container node + strategy: + fail-fast: false # Prevent crash dependencies: keeps other unit operations processing if one page token fails + matrix: + # Converts the Python string output back into a live GitHub Actions looping engine array schema + target: ${{ fromJson(needs.parse-blog.outputs.targets) }} + steps: + - name: Set Facebook Credentials Dynamically # Step 1: Conditional evaluation mapping loop tokens to secure Vault entries + id: set_tokens # ID marker allowing the cURL step to fetch the selected dynamic parameters run: | - TARGET="${{ steps.blog_data.outputs.target_page }}" - if [ "$TARGET" = "TECH" ]; then - echo "page_id=${{ secrets.FB_PAGE_ID_TECH }}" >> $GITHUB_OUTPUT - echo "access_token=${{ secrets.FB_ACCESS_TOKEN_TECH }}" >> $GITHUB_OUTPUT - elif [ "$TARGET" = "LIFESTYLE" ]; then - echo "page_id=${{ secrets.FB_PAGE_ID_LIFESTYLE }}" >> $GITHUB_OUTPUT - echo "access_token=${{ secrets.FB_ACCESS_TOKEN_LIFESTYLE }}" >> $GITHUB_OUTPUT + TARGET="${{ matrix.target }}" # Extract current matrix iteration string item token value + if [ "$TARGET" = "TROOP_331" ]; then + echo "page_id=${{ secrets.FB_PAGE_ID_TROOP_331 }}" >> $GITHUB_OUTPUT # Maps the unique identification numbers + echo "access_token=${{ secrets.FB_ACCESS_TOKEN_TROOP_331 }}" >> $GITHUB_OUTPUT # Maps the specific system token + elif [ "$TARGET" = "PACK_303" ]; then + echo "page_id=${{ secrets.FB_PAGE_ID_PACK_303 }}" >> $GITHUB_OUTPUT + echo "access_token=${{ secrets.FB_ACCESS_TOKEN_PACK_303 }}" >> $GITHUB_OUTPUT + elif [ "$TARGET" = "CREW_303" ]; then + echo "page_id=${{ secrets.FB_PAGE_ID_CREW_303 }}" >> $GITHUB_OUTPUT + echo "access_token=${{ secrets.FB_ACCESS_TOKEN_CREW_303 }}" >> $GITHUB_OUTPUT + elif [ "$TARGET" = "TROOP_303" ]; then + echo "page_id=${{ secrets.FB_PAGE_ID_TROOP_303 }}" >> $GITHUB_OUTPUT + echo "access_token=${{ secrets.FB_ACCESS_TOKEN_TROOP_303 }}" >> $GITHUB_OUTPUT else - echo "page_id=${{ secrets.FB_PAGE_ID_DEFAULT }}" >> $GITHUB_OUTPUT + echo "page_id=${{ secrets.FB_PAGE_ID_DEFAULT }}" >> $GITHUB_OUTPUT # Fallback mappings if special exceptions occur echo "access_token=${{ secrets.FB_ACCESS_TOKEN_DEFAULT }}" >> $GITHUB_OUTPUT fi - - name: Send Link to Selected Facebook Page - if: steps.blog_data.outputs.has_new_post == 'true' + - name: Send Link to Selected Facebook Page # Step 2: Dispatches feed payload securely onto the Facebook Graph API endpoints run: | - BLOG_URL="https://brownsburgscouts.org{{ steps.blog_data.outputs.slug }}" - MESSAGE="✍️ New Blog Post: ${{ steps.blog_data.outputs.title }} + # Formulates absolute permalink layout references ensuring the /blog directory prefix matches your structure + BLOG_URL="https://brownsburgscouts.org{{ needs.parse-blog.outputs.slug }}" + + # Sets up text message string template payloads using emoticons and line skip variables + MESSAGE="✍️ New Blog Post: ${{ needs.parse-blog.outputs.title }} Read the full article here: $BLOG_URL" + # Fires cURL POST web request arguments hitting Meta Graph API servers, routing items onto dynamic destinations curl -X POST "https://facebook.com{{ steps.set_tokens.outputs.page_id }}/feed" \ - -d "message=$(echo "$MESSAGE")" \ + -d "message=$MESSAGE" \ -d "link=$BLOG_URL" \ - -d "access_token=${{ steps.set_tokens.outputs.access_token }}" + -d "access_token=${{ secrets.FB_ACCESS_TOKEN_DEFAULT }}" # Authorizes operations utilizing isolated access tokens diff --git a/.github/workflows/content-management.yml b/.github/workflows/content-management.yml index 9e6d6af..c547992 100644 --- a/.github/workflows/content-management.yml +++ b/.github/workflows/content-management.yml @@ -1,113 +1,125 @@ -name: Content Management Automation +# ============================================================================== +# GitHub Actions Automation Workflow: Content Management +# Orchestrates automatic Pull Request creation when issues with specific labels +# ('blog', 'docs', or 'new-author') are submitted via issue form templates. +# ============================================================================== -on: - issues: - types: [opened] +name: Content Management Automation # Friendly name displayed in the GitHub Actions tab -jobs: +on: # Defines the event hook that triggers this workflow + issues: # Listens to issue activity + types: [opened] # Restricts execution to run only when an issue is newly created + +jobs: # Contains the operational pipelines/jobs to execute + # ---------------------------------------------------------------------------- # WORKFLOW 1: Generate Blog Post from Issue + # ---------------------------------------------------------------------------- create-post: + # Execution gate: only runs if the newly opened issue has the 'blog' label applied if: contains(github.event.issue.labels.*.name, 'blog') - runs-on: ubuntu-latest + runs-on: ubuntu-latest # Provisions a fresh virtual machine host environment steps: - - name: Checkout Code - uses: actions/checkout@v6 + - name: Checkout Code # Step 1: Pulls down your repository code onto the runner + uses: actions/checkout@v6 # Uses the standard repository checkout action - - name: Generate GitHub App Token - id: app-token - uses: actions/create-github-app-token@v3 + - name: Generate GitHub App Token # Step 2: Obtains elevated permissions for automation steps + id: app-token # ID designation used to query output data in later fields + uses: actions/create-github-app-token@v3 # Uses GitHub App credentials to bypass workflow limits with: - client-id: ${{ secrets.APP_ID }} + client-id: ${{ secrets.APP_ID }} # References system secret credentials private-key: ${{ secrets.APP_PRIVATE_KEY }} - - name: Parse Issue Form - id: parse + - name: Parse Issue Form # Step 3: Extracts individual form values from the issue + id: parse # ID notation used to fetch variables like jsonString uses: stefanbuck/github-issue-parser@v3 with: - template-path: .github/ISSUE_TEMPLATE/01-new-blog-post.yml + template-path: .github/ISSUE_TEMPLATE/01-new-blog-post.yml # Path targeting the source form template - - name: Set up Python + - name: Set up Python # Step 4: Installs runtime environment for processing scripts uses: actions/setup-python@v6 with: - python-version: "3.x" + python-version: "3.x" # Always provisions the latest minor build variation of Python 3 - - name: Install Dependencies + - name: Install Dependencies # Step 5: Installs mandatory libraries for image/text handling run: | python -m pip install --upgrade pip pip install Pillow python-dateutil - - name: Process Issue Data and Assets (Python Optimizer) + - name: Process Issue Data and Assets (Python Optimizer) # Step 6: Executes data conversion script id: process env: - ISSUE_JSON: ${{ steps.parse.outputs.jsonString }} + ISSUE_JSON: ${{ steps.parse.outputs.jsonString }} # Maps extracted form fields to environment variables run: | - python .github/scripts/generate_post.py + python .github/scripts/generate_post.py # Script execution target file - - name: Create Pull Request - id: cpr + - name: Create Pull Request # Step 7: Opens a staging PR with the new post files + id: cpr # ID mapping used to fetch the PR URL for issue commenting uses: peter-evans/create-pull-request@v8 with: - token: ${{ steps.app-token.outputs.token }} + token: ${{ steps.app-token.outputs.token }} # Grants PR permissions via the generated App token commit-message: "feat(blog): add new post from issue #${{ github.event.issue.number }}" - branch: "automation/issue-${{ github.event.issue.number }}-${{ env.BLOG_FILENAME }}" - title: "feat(blog): ${{ github.event.issue.title }}" - body: | + branch: "automation/issue-${{ github.event.issue.number }}-${{ env.BLOG_FILENAME }}" # Isolated branch path + title: "feat(blog): ${{ github.event.issue.title }}" # Sets the issue title as the PR title + body: | # Description text posted within the generated PR body description This Pull Request was automatically generated from Issue #${{ github.event.issue.number }}. Closes #${{ github.event.issue.number }} - delete-branch: true - labels: | + delete-branch: true # Deletes the branch once the PR is merged into main + labels: | # Automatically appends tracking tags to the PR blog - - name: Comment on Issue with PR Link - if: steps.cpr.outputs.pull-request-number != '' + - name: Comment on Issue with PR Link # Step 8: Provides clear notification link back to the user + if: steps.cpr.outputs.pull-request-number != '' # Guard clause ensuring the PR was successfully opened uses: peter-evans/create-or-update-comment@v5 with: token: ${{ steps.app-token.outputs.token }} issue-number: ${{ github.event.issue.number }} - body: | + body: | # Formats notice text targeting the native conversation section 🎉 Success! A staging branch has been created. Your blog post is ready for review here:👉 ${{ steps.cpr.outputs.pull-request-url }} + # ---------------------------------------------------------------------------- # WORKFLOW 2: Generate Document from Issue + # ---------------------------------------------------------------------------- create-doc: + # Execution gate: only runs if the newly opened issue has the 'docs' label applied if: contains(github.event.issue.labels.*.name, 'docs') - runs-on: ubuntu-latest + runs-on: ubuntu-latest # Provisions a fresh virtual machine host environment steps: - - name: Checkout Code + - name: Checkout Code # Step 1: Pulls down your repository code onto the runner uses: actions/checkout@v6 - - name: Generate GitHub App Token + - name: Generate GitHub App Token # Step 2: Obtains elevated permissions for automation steps id: app-token uses: actions/create-github-app-token@v3 with: client-id: ${{ secrets.APP_ID }} private-key: ${{ secrets.APP_PRIVATE_KEY }} - - name: Parse Issue Form + - name: Parse Issue Form # Step 3: Extracts individual form values from the issue id: parse uses: stefanbuck/github-issue-parser@v3 with: - template-path: ./github/ISSUE_TEMPLATE/02-new-document.yml + template-path: ./github/ISSUE_TEMPLATE/02-new-document.yml # Targeting the document form template - - name: Set up Python + - name: Set up Python # Step 4: Installs runtime environment for processing scripts uses: actions/setup-python@v6 with: python-version: "3.x" - - name: Install Dependencies + - name: Install Dependencies # Step 5: Installs mandatory libraries for file handling run: | python -m pip install --upgrade pip pip install Pillow - - name: Process Issue Data and Assets (Python Optimizer) + - name: Process Issue Data and Assets (Python Optimizer) # Step 6: Executes file conversion script id: process env: - ISSUE_JSON: ${{ steps.parse.outputs.jsonString }} + ISSUE_JSON: ${{ steps.parse.outputs.jsonString }} # Maps extracted form fields to environment variables run: | python .github/scripts/generate_docs.py - - name: Create Pull Request + - name: Create Pull Request # Step 7: Opens a staging PR with the new documentation files id: cpr uses: peter-evans/create-pull-request@v8 with: @@ -122,7 +134,7 @@ jobs: labels: | docs - - name: Comment on Issue with PR Link + - name: Comment on Issue with PR Link # Step 8: Provides clear notification link back to the user if: steps.cpr.outputs.pull-request-number != '' uses: peter-evans/create-or-update-comment@v5 with: @@ -133,46 +145,49 @@ jobs: Your document is ready for review here:👉 ${{ steps.cpr.outputs.pull-request-url }} + # ---------------------------------------------------------------------------- # WORKFLOW 3: Process New Author Request + # ---------------------------------------------------------------------------- add-author-and-pr: + # Execution gate: only runs if the newly opened issue has the 'new-author' label applied if: contains(github.event.issue.labels.*.name, 'new-author') - runs-on: ubuntu-latest + runs-on: ubuntu-latest # Provisions a fresh virtual machine host environment steps: - - name: Checkout Repo + - name: Checkout Repo # Step 1: Pulls down your repository code onto the runner uses: actions/checkout@v6 - - name: Generate Token + - name: Generate Token # Step 2: Obtains elevated permissions for automation steps id: app-token uses: actions/create-github-app-token@v3 with: client-id: ${{ secrets.APP_ID }} private-key: ${{ secrets.APP_PRIVATE_KEY }} - - name: Parse Issue Form + - name: Parse Issue Form # Step 3: Extracts profile fields from onboarding issue id: parse uses: stefanbuck/github-issue-parser@v3 with: - template-path: .github/ISSUE_TEMPLATE/03-new-blog-author.yml + template-path: .github/ISSUE_TEMPLATE/03-new-blog-author.yml # Targeting onboarding template - - name: Set up Python + - name: Set up Python # Step 4: Installs runtime environment for onboarding script uses: actions/setup-python@v6 with: python-version: "3.x" - - name: Install Dependencies + - name: Install Dependencies # Step 5: Installs text/image optimization libraries run: | python -m pip install --upgrade pip pip install Pillow PyYAML python-dateutil - - name: Process Author and Update Files + - name: Process Author and Update Files # Step 6: Appends dataset and edits dropdown lists id: process_files env: - ISSUE_DATA: ${{ steps.parse.outputs.jsonString }} + ISSUE_DATA: ${{ steps.parse.outputs.jsonString }} # Context identifier variable name target run: | python .github/scripts/add_author.py - - name: Create Pull Request - if: success() + - name: Create Pull Request # Step 7: Opens a staging PR updating configuration maps + if: success() # Condition check: ensures data steps finished with exit code 0 uses: peter-evans/create-pull-request@v8 with: token: ${{ steps.app-token.outputs.token }} @@ -181,7 +196,7 @@ jobs: body: | This PR automatically handles three tasks: 1. Downloads, optimizes, and names the avatar image matching the unique author slug. - 2. Adds the new author data mapping properties into `blog/authors.yml`. + 2. Adds the new author data mapping properties into blog/authors.yml. 3. Re-generates and sorts the author dropdown options inside the issue templates. Closes #${{ github.event.issue.number }}. CODEOWNERS have been automatically assigned to review. @@ -189,33 +204,41 @@ jobs: delete-branch: true labels: | new-author - - - name: Comment on Success - if: success() + + - name: Comment on Success # Step 8: Alerts author that review process has begun + if: success() # Condition check: executes only if PR creation succeeds uses: peter-evans/create-or-update-comment@v5 with: token: ${{ steps.app-token.outputs.token }} issue-number: ${{ github.event.issue.number }} body: | - Hi there! An automated Pull Request has been generated to add you to the blog authors file and update our submission dropdown tools. The project's CODEOWNERS have been notified to review and merge the changes. Once merged, your author profile and dropdown options will be active! - - - name: Handle Duplicate / Failure - if: failure() - uses: actions/github-script@v9 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | + Hi there! An automated Pull Request has been generated to add you to + the blog authors file and update our submission dropdown tools. The + project's CODEOWNERS have been notified to review and merge the + changes. Once merged, your author profile and dropdown options will + be active! + + - name: Handle Duplicate / Failure # Step 9: Rejects entry and handles automated clean close + if: failure() # Condition check: executes if scripts fail (e.g., duplicate errors) + uses: actions/github-script@v9 # Leverages native JavaScript inline engine execution tools + with: + github-token: ${{ secrets.GITHUB_TOKEN }} # Standard repo token is fine for closure actions + script: | # Runs inline Javascript logic against Github API layers const issueNumber = context.issue.number; + // Post notification error explanation comment to issue chat log await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: issueNumber, - body: "🛑 **Registration Error:** It looks like an author profile with this name already exists in `blog/authors.yml`. Duplicate entries are not allowed. This issue will now be closed automatically." + body: "🛑 Registration Error: It looks like an author profile with + this name already exists in blog/authors.yml. Duplicate entries are + not allowed. This issue will now be closed automatically." }); + // Immediately close out the issue and label it as uncompleted await github.rest.issues.update({ owner: context.repo.owner, repo: context.repo.repo, issue_number: issueNumber, state: 'closed', state_reason: 'not_planned' - }); + }); \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index e2b5f9b..e3e219f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,4 @@ +// .vscode/settings.json { "[markdown]": { "editor.defaultFormatter": "esbenp.prettier-vscode", diff --git a/note.md b/note.md new file mode 100644 index 0000000..532e402 --- /dev/null +++ b/note.md @@ -0,0 +1,130 @@ +To connect your automated pipeline to Facebook, you need to configure 8 distinct +Repository Secrets in your GitHub repository settings. The process requires +creating an app on the Meta for Developers portal, assigning permissions, +generating system user tokens that never expire, and saving those keys into +GitHub. [1, 2] + +--- + +## Step 1: The Repository Secrets Checklist + +You must add these exact pairs of names to your GitHub Secrets inventory: + +| Scouting Unit | Page ID Secret Name | Access Token Secret Name | +| -------------- | -------------------- | ---------------------------------------------------- | +| Troop 303 | FB_PAGE_ID_TROOP_303 | FB_ACCESS_TOKEN_TROOP_303 | +| Troop 331 | FB_PAGE_ID_TROOP_331 | FB_ACCESS_TOKEN_TROOP_331 | +| Pack 303 | FB_PAGE_ID_PACK_303 | FB_ACCESS_TOKEN_PACK_303 | +| Crew 303 | FB_PAGE_ID_CREW_303 | FB_ACCESS_TOKEN_CREW_303 | +| Fallback / All | FB_PAGE_ID_DEFAULT | FB_ACCESS_TOKEN_DEFAULT (Point to your primary page) | + +--- + +## Step 2: How to Find Your Facebook Page IDs + +Finding your Page IDs is simple and does not require developer tools: + +1. Log into Facebook and switch your profile to the target page (e.g., Troop + 303). +2. Go to the page's profile view and look at the URL in your browser address + bar. The long number at the very end of the URL is your Page ID (e.g., + https://facebook.com...). +3. Alternatively, click the About tab on your page, scroll down to Page + Transparency, and copy the number listed there. [3] + +--- + +## Step 3: Create a System App on Meta for Developers + +Standard developer access tokens expire after 60 days. To generate permanent +tokens that never time out, you must create a Meta App: + +1. Navigate to [facebook.com](https://developers.facebook.com/) and log in with + your primary Facebook account. +2. Click My Apps in the top right, then click Create App. +3. Select Other as the app use case, click Next, and choose Business as your + app type. +4. Give your app a name (e.g., Scout Site Automation), select your Business + Account from the dropdown menu, and click Create App. [4, 5, 6, 7] + +--- + +## Step 4: Generate Permanent System User Access Tokens + +1. From your Meta App dashboard, look at the sidebar and click Tools → Business + Settings. This opens your Meta Business Suite dashboard. +2. In the Business Settings sidebar, click Users → System Users. [8, 9] +3. Click Add to provision a new system user. Name it GitHub Actions Router and + set the role to Admin. +4. Select your newly created System User, click Add Assets, choose Pages, + select all your unit pages, and toggle on Full Control (Everything). Save + your changes. [10] +5. With the System User still highlighted, click the Generate New Token button. +6. Select your App from the dropdown list, and check these exact permission + scopes: + +- pages_manage_posts + - pages_read_engagement + - pages_show_list + +7. Click Generate Token. Copy this string immediately. This is your permanent + Page Access Token. Repeat this generation step for each unique page asset if + your pages are held across different business accounts. [11, 12] + +--- + +## Step 5: Save the Values in GitHub Secrets + +Once you have gathered all your Page IDs and token strings, encrypt them within +your repository configuration panel: + +1. Open your web browser and go to your target GitHub Repository. +2. Click the Settings tab (the gear icon at the top of the interface menu). +3. Scroll down the left sidebar to the Security header block, click Secrets and + variables, and select Actions. +4. Click the green New repository secret button. +5. In the Name input field, type the exact variable name (e.g., + FB_ACCESS_TOKEN_TROOP_303). +6. Paste the corresponding token string value into the large Secret text area + box. +7. Click Add secret to encrypt and lock down the key. Repeat this process until + all 8 variable properties match your target configurations. [13, 14, 15, 16, + 17] + +If your script triggers but throws a "Permission Denied" log error during +testing, I can show you how to use the Meta Access Token Debugger tool to verify +your token parameters. Would you like me to map out those validation steps? + +[1] +[https://gitprotect.io](https://gitprotect.io/blog/how-to-safely-store-secrets-in-github/) +[2] +[https://baserow.io](https://baserow.io/user-docs/configure-facebook-for-oauth-2-sso) +[3] +[https://www.socialmediaexaminer.com](https://www.socialmediaexaminer.com/4-ways-to-use-google-tag-manager-with-facebook/) +[4] +[https://www.digittrix.com](https://www.digittrix.com/scripts/facebook-login-implementation-in-react-and-nodejs) +[5] +[https://docs.n8n.io](https://docs.n8n.io/integrations/builtin/credentials/facebookapp/) +[6] +[https://www.facebook.com](https://www.facebook.com/MetaforDevelopers/videos/get-started-with-the-messenger-api-for-instagram/389112236490456/) +[7] +[https://ckmobile.medium.com](https://ckmobile.medium.com/nextauth-part-8-facebook-provider-2b058b0ae7bb) +[8] +[https://developers.facebook.com](https://developers.facebook.com/documentation/facebook-login/guides/access-tokens) +[9] +[https://fivetran.com](https://fivetran.com/docs/activations/destinations/available-destinations/facebook-ads) +[10] +[https://gist.github.com](https://gist.github.com/michaelkarrer81/88fbc36d99a8a32a83f3efe234f7690a) +[11] +[https://help.zscaler.com](https://help.zscaler.com/uvm/configuring-github-advanced-security-connector) +[12] +[https://mixedanalytics.com](https://mixedanalytics.com/knowledge-base/import-facebook-page-data-to-google-sheets/) +[13] +[https://www.linkedin.com](https://www.linkedin.com/pulse/git-github-punit-dhiman-wouse) +[14] +[https://chrisjhart.com](https://chrisjhart.com/Creating-A-Simple-Free-Blog-Hugo/) +[15] [https://github.com](https://github.com/orgs/community/discussions/170965) +[16] +[https://www.storylane.io](https://www.storylane.io/tutorials/how-to-add-secrets-to-github) +[17] +[https://www.linkedin.com](https://www.linkedin.com/learning/data-pipeline-automation-with-github-actions-using-r-and-python/setting-secrets-and-environment-variables) diff --git a/sidebarDocs.js b/sidebarDocs.js index d317137..482fcec 100644 --- a/sidebarDocs.js +++ b/sidebarDocs.js @@ -15,23 +15,30 @@ * @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */ export default { + // Defines the individual sidebar identifier name referenced inside docusaurus.config.js sidebarDocs: [ + // --------------------------------------------------------------------- + // CATEGORY 1: General cross-unit documentation mapping block + // --------------------------------------------------------------------- { - type: 'category', - label: 'General Documents', + type: 'category', // Renders an expandable multi-item folder dropdown block + label: 'General Documents', // Public text header label displayed inside the UI sidebar link: { - type: 'generated-index', - title: 'General Documents', - description: 'General documents applicable to multiple units', - slug: '/general' + type: 'generated-index', // Instructs Docusaurus to auto-generate a landing dashboard view page + title: 'General Documents', // Headline title text used inside the auto-generated dashboard view + description: 'General documents applicable to multiple units', // Explanatory subtitle string + slug: '/general' // Base web URL route path mapping address for this index node }, items: [ { - type: 'autogenerated', - dirName: 'general' + type: 'autogenerated', // Enables automatic scanning of your local file directory trees + dirName: 'general' // Recursively imports files located inside the "docs/general/" path } ] }, + // --------------------------------------------------------------------- + // CATEGORY 2: Crew 303 documentation folder block + // --------------------------------------------------------------------- { type: 'category', label: 'Crew 303', @@ -44,10 +51,13 @@ export default { items: [ { type: 'autogenerated', - dirName: 'crew-303' + dirName: 'crew-303' // Recursively imports files located inside the "docs/crew-303/" path } ] }, + // --------------------------------------------------------------------- + // CATEGORY 3: Pack 303 documentation folder block + // --------------------------------------------------------------------- { type: 'category', label: 'Pack 303', @@ -60,10 +70,13 @@ export default { items: [ { type: 'autogenerated', - dirName: 'pack-303' + dirName: 'pack-303' // Recursively imports files located inside the "docs/pack-303/" path } ] }, + // --------------------------------------------------------------------- + // CATEGORY 4: Troop 303 documentation folder block + // --------------------------------------------------------------------- { type: 'category', label: 'Troop 303', @@ -76,10 +89,13 @@ export default { items: [ { type: 'autogenerated', - dirName: 'troop-303' + dirName: 'troop-303' // Recursively imports files located inside the "docs/troop-303/" path } ] }, + // --------------------------------------------------------------------- + // CATEGORY 5: Troop 331 documentation folder block + // --------------------------------------------------------------------- { type: 'category', label: 'Troop 331', @@ -92,9 +108,9 @@ export default { items: [ { type: 'autogenerated', - dirName: 'troop-331' + dirName: 'troop-331' // Recursively imports files located inside the "docs/troop-331/" path } ] } ] -} \ No newline at end of file +} diff --git a/src/components/BlogCard/index.jsx b/src/components/BlogCard/index.jsx index 9ac6029..92c3f9d 100644 --- a/src/components/BlogCard/index.jsx +++ b/src/components/BlogCard/index.jsx @@ -14,12 +14,12 @@ */ import React from "react"; -import Link from '@docusaurus/Link'; -import clsx from "clsx"; -import styles from './styles.module.css'; -import Heading from '@theme/Heading'; -import recentPosts from '@site/.docusaurus/recent-posts.json'; -import useBaseUrl from "@docusaurus/useBaseUrl"; +import Link from '@docusaurus/Link'; // Docusaurus optimized router link component to prevent full-page reloads +import clsx from "clsx"; // Utility for conditionally joining CSS class names together cleanly +import styles from './styles.module.css'; // Scoped CSS Modules styling sheet for this specific layout component +import Heading from '@theme/Heading'; // Swappable theme heading component supporting semantic HTML structures +import recentPosts from '@site/.docusaurus/recent-posts.json'; // Pre-built local JSON database payload containing recent post metadata +import useBaseUrl from "@docusaurus/useBaseUrl"; // Appends the site's configured baseUrl configuration prefix to static paths /** * Localizes an ISO date string to "MMM DD, YYYY" using UTC to prevent timezone offsets. @@ -28,13 +28,13 @@ import useBaseUrl from "@docusaurus/useBaseUrl"; * @returns {string} The formatted date. */ const formatDate = (isoString) => { - const date = new Date(isoString); - return new Intl.DateTimeFormat('en-US', { - month: 'short', - day: 'numeric', - year: 'numeric', - timeZone: 'UTC' - }).format(date); + const date = new Date(isoString); // Instantiates a native JavaScript Date engine object + return new Intl.DateTimeFormat('en-US', { // Leverages ECMAScript Internationalization API for lightweight date building + month: 'short', // Formats month into a 3-letter shorthand descriptor (e.g., "Jan") + day: 'numeric', // Formats day into standard integer characters (e.g., "18") + year: 'numeric', // Formats year into a 4-digit layout block (e.g., "2026") + timeZone: 'UTC' // Enforces universal time to guard against localized browser skew shifts + }).format(date); // Commits parsing transformations to output a finalized string wrapper }; /** @@ -49,6 +49,7 @@ const formatDate = (isoString) => { * @param {Object} props.frontmatter - Unprocessed raw frontmatter fields containing configuration like custom cover images. */ function BlogCard({ permalink, title, date, authors, tags }) { + // Pre-caches the global server asset path prefixing mapping rule for the default background const fallbackDefaultImage = useBaseUrl('img/blog/default-blog-cover.webp'); // 1. Isolate the base slug name by stripping the leading "/blog" and trailing slashes @@ -67,16 +68,20 @@ function BlogCard({ permalink, title, date, authors, tags }) { let resolvedCoverUrl; try { // 2. Webpack looks inside the static folder during compile time + // Dynamically checks for the presence of an optimized webp illustration file asset block at compilation time resolvedCoverUrl = require(`@site/static/img/blog/${folderName}/cover.webp`).default; } catch (err) { // 3. If file doesn't exist, it instantly uses the fallback at build time + // Fallback error-handling catching missing directory trees to seamlessly inject standard cards instead resolvedCoverUrl = fallbackDefaultImage; } return ( + // Infuses standard Infima CSS grid infrastructure properties (allocating 3 out of 12 columns per entry)
+ {/* 1. Card Image Header Frame Container */}
- {/* 2. Card Body */} + {/* 2. Card Body Content Presentation Wrapper */}
+ {/* Converts structural timestamps into beautiful localized textual outputs */} {formatDate(date)}

{title}

+ {/* Iterates through a limited section slice of taxonomies to layout classification labels */}
{tags.slice(0, 3).map((tag, idx) => ( @@ -98,23 +105,27 @@ function BlogCard({ permalink, title, date, authors, tags }) { ))}
- {/* 3. Footer: Authors & Read More */} + {/* 3. Footer: Authors Avatar Profiles Stack & Direct Action Link */}
+ {/* Loops over the individual post authors block data mapping variables */} {authors.map((author, idx) => ( {author.name} ))} + {/* Dynamic summary phrase adjustment matching plural criteria boundaries */} {authors.length === 1 ? authors[0].name : `${authors.length} Authors`}
+ {/* Visual element anchor pointing to the comprehensive blog post review content view */} Read →
@@ -123,27 +134,35 @@ function BlogCard({ permalink, title, date, authors, tags }) { ); } +/** + * Root component that maps out the collective layout grid workspace dashboard view. + * @public + * @returns {JSX.Element} Structural framework rendering recent post items. + */ export default function HomepageBlogCards() { return ( <> + {/* Layout container aligning content cleanly along central layout coordinate nodes */}
Recent Adventures
+ {/* Standard center-aligned responsive Infima CSS grid structural layout row box wrapper */}
+ {/* Dynamically steps down through the collection items data payload to inject the components grid */} {recentPosts.map((post) => ( ))}
); -} \ No newline at end of file +} diff --git a/src/components/BlogCard/styles.module.css b/src/components/BlogCard/styles.module.css index c1636b8..ddfa475 100644 --- a/src/components/BlogCard/styles.module.css +++ b/src/components/BlogCard/styles.module.css @@ -1 +1,175 @@ -.blogCard{display:flex;flex-direction:column;height:100%;background:var(--ifm-card-background-color);border:1px solid var(--ifm-color-emphasis-200);border-radius:12px;overflow:hidden;transition:transform .2s,box-shadow .2s,border-color .2s;text-decoration:none!important;color:inherit}.cardMeta,.date{display:flex;margin-bottom:10px;font-size:.85rem;color:var(--ifm-color-emphasis-600)}.blogCard:hover{transform:translateY(-5px);box-shadow:var(--global-box-shadow);border-color:var(--ifm-color-primary)}.cardHeader{height:160px;overflow:hidden}.cardImage{width:100%;height:100%;object-fit:cover;transition:transform .5s}.blogCard:hover .cardImage{transform:scale(1.05)}.cardBody{padding:1.5rem;display:flex;flex-direction:column;flex-grow:1}.date{justify-content:space-between;align-items:center}.cardMeta{gap:8px;align-items:center}.tagPill{background-color:var(--ifm-color-primary-lightest);color:var(--ifm-color-primary-dark);padding:2px 8px;border-radius:12px;font-size:.75rem;font-weight:700}.cardTitle{font-size:1.25rem;font-weight:700;margin-bottom:.5rem;line-height:1.3}.cardDescription{font-size:.9rem;color:var(--ifm-color-emphasis-700);margin-bottom:1.5rem;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}.cardFooter{margin-top:auto;display:flex;align-items:center;justify-content:space-between;padding-top:1rem;border-top:1px solid var(--ifm-color-emphasis-200)}.authorStack{display:flex;align-items:center}.authorAvatar{width:28px;height:28px;border-radius:50%;border:2px solid var(--ifm-card-background-color);margin-right:-8px;object-fit:cover}.authorAvatar:last-of-type{margin-right:8px}.authorName{font-size:.85rem;font-weight:600;margin-left:5px}.readMore{color:var(--ifm-color-primary);font-weight:700;font-size:.9rem}.newBadge{position:absolute;top:12px;right:12px;background-color:var(--ifm-color-primary);color:var(--ifm-color-white);padding:4px 10px;border-radius:20px;font-size:.7rem;font-weight:700;letter-spacing:1px;z-index:10;box-shadow:var(--global-box-shadow);pointer-events:none;animation:2s infinite pulse}@keyframes pulse{0%,100%{transform:scale(1)}50%{transform:scale(1.05)}} \ No newline at end of file +/* ============================================================================== + CSS Module Stylesheet: Blog Post Display Grid Cards + Structures responsive grid blocks integrating with Docusaurus Infima variables. + ============================================================================== */ + +/* Main overarching container component card link frame wrapper */ +.blogCard { + display: flex; + flex-direction: column; /* Arranges child items inside a vertical top-to-bottom stack layout */ + height: 100%; /* Spans full height of grid cell blocks ensuring equal rows layout grid matching */ + background: var(--ifm-card-background-color); /* Extracts Docusaurus central light/dark UI design mode layout backdrop colors */ + border: 1px solid var(--ifm-color-emphasis-200); /* Thin subtle bounding frame line outline */ + border-radius: 12px; /* Soft modern curved structural card corner geometry edges */ + overflow: hidden; /* Clips inside assets like header photos so they do not bleed past the corner curve framework */ + transition: transform .2s, box-shadow .2s, border-color .2s; /* Clean hardware-accelerated transformation frame timings */ + text-decoration: none !important; /* Suppresses native browser text underline styling decorations across content child blocks */ + color: inherit; /* Absorbs upstream site font palette color configurations */ +} + +/* Collective mapping grouping mutual visual metadata font sizing attributes together */ +.cardMeta, +.date { + display: flex; + margin-bottom: 10px; + font-size: .85rem; /* Clean readable text typography size downscale properties */ + color: var(--ifm-color-emphasis-600); /* Secondary accent text gray typography palette rule */ +} + +/* Hover tracking transition rules modifying layout variables when a cursor glides over the card */ +.blogCard:hover { + transform: translateY(-5px); /* Slightly lifts the card upward along the vertical viewport coordinate mapping nodes */ + box-shadow: var(--global-box-shadow); /* Projects deep backdrop shadow values for three-dimensional visual depth look */ + border-color: var(--ifm-color-primary); /* Switches border strokes to match the core branding primary color scheme configuration */ +} + +/* Framing boundary cell holding the background blog post illustration cover image asset */ +.cardHeader { + height: 160px; /* Fast locked boundary height line limiting initial graphic dimension allocations */ + overflow: hidden; /* Conceals structural scaling animations extending past outer boundaries */ +} + +/* Core background cover image presentation property class layout values */ +.cardImage { + width: 100%; + height: 100%; + object-fit: cover; /* Scales and centers illustration frames without warping aspect ratio parameters */ + transition: transform .5s; /* Smooth, slow duration rule handling image growth zoom calculations */ +} + +/* Cinematic zoom animation rule scaling the header illustration inward when user handles hover items */ +.blogCard:hover .cardImage { + transform: scale(1.05); /* Extends file image asset tracking dimensions outwards by exactly 5% scale margins */ +} + +/* Inner content typography compartment box boundary container */ +.cardBody { + padding: 1.5rem; /* Generates equal, deep margins formatting readable inside cell spacing padding bounds */ + display: flex; + flex-direction: column; + flex-grow: 1; /* Forces internal cell block spaces to inflate outward to fill lingering empty footer fields */ +} + +/* Arranges content structures horizontally across endpoints inside the date tracking row block */ +.date { + justify-content: space-between; /* Separates child parameters uniformly across opposite ends of the container */ + align-items: center; /* Locks target items along centralized vertical coordinate balancing nodes */ +} + +/* Taxonomy tags row structural configuration layout wrapper */ +.cardMeta { + gap: 8px; /* Enforces static gaps separating neighboring category items without margins overrides */ + align-items: center; +} + +/* Customized visual badge pills displaying classification tags (e.g., Troop 303) */ +.tagPill { + background-color: var(--ifm-color-primary-lightest); /* Light branding tone backdrop fill */ + color: var(--ifm-color-primary-dark); /* Dark contrasting text color scheme ensuring accessibility visibility rules */ + padding: 2px 8px; /* Compact spacing frames enclosing text values */ + border-radius: 12px; /* Capsule layout rounded side curves styling profile */ + font-size: .75rem; + font-weight: 700; /* Strong dense text formatting parameters */ +} + +/* Article headline title text layout configuration parameters */ +.cardTitle { + font-size: 1.25rem; + font-weight: 700; + margin-bottom: .5rem; + line-height: 1.3; /* Tightens spatial line stacks preventing multi-line headers from drifting apart */ +} + +/* Summary paragraph excerpt text box formatting framework boundaries */ +.cardDescription { + font-size: .9rem; + color: var(--ifm-color-emphasis-700); + margin-bottom: 1.5rem; + display: -webkit-box; /* Invokes legacy multi-line truncation support properties across Webkit rendering frameworks */ + -webkit-line-clamp: 3; /* Hard limits narrative paragraphs from extending past a maximum boundary count of 3 text rows */ + -webkit-box-orient: vertical; /* Dictates alignment truncation axis direction processing parameters */ + overflow: hidden; /* Truncates trailing overflows and appends native ellipsis points (...) automatically */ +} + +/* Bottom status panel component block anchoring authors metrics and read buttons */ +.cardFooter { + margin-top: auto; /* Leverages flex push tracking fields to pin footer arrays exactly to the card floor line */ + display: flex; + align-items: center; + justify-content: space-between; + padding-top: 1rem; + border-top: 1px solid var(--ifm-color-emphasis-200); /* Separation accent line divider dividing body from foot contents */ +} + +/* Collates multiple profile faces closely together using overlapping configurations */ +.authorStack { + display: flex; + align-items: center; +} + +/* Multi-author portrait avatar rendering styling rules */ +.authorAvatar { + width: 28px; + height: 28px; + border-radius: 50%; /* Forces standard source square images to resolve as perfect circular layout graphics */ + border: 2px solid var(--ifm-card-background-color); /* Clean structural border matching card backing to mask overlaps cleanly */ + margin-right: -8px; /* Negative horizontal alignment margins shifting portraits leftwards into tight stack arrangements */ + object-fit: cover; +} + +/* Reset rules targeting terminal profile items inside the stacked face array layers */ +.authorAvatar:last-of-type { + margin-right: 8px; /* Negates negative margins for final elements to prevent profile name strings from overlapping faces */ +} + +/* Post creator description name details layout block text string elements */ +.authorName { + font-size: .85rem; + font-weight: 600; + margin-left: 5px; +} + +/* Read more action call text label link components */ +.readMore { + color: var(--ifm-color-primary); + font-weight: 700; + font-size: .9rem; +} + +/* Announcement tag badge overlaying the card workspace boundaries for urgent new items entries */ +.newBadge { + position: absolute; /* Breaks element away from baseline DOM document flows to place relative to coordinate points */ + top: 12px; /* Spacing offset position from card roof ceiling line */ + right: 12px; /* Spacing offset position from card right margin boundary wall */ + background-color: var(--ifm-color-primary); + color: var(--ifm-color-white); + padding: 4px 10px; + border-radius: 20px; + font-size: .7rem; + font-weight: 700; + letter-spacing: 1px; /* Extends characters outwards to boost overall visual tracking legibility features */ + z-index: 10; /* Forces tag to lift above baseline card contents to avoid image rendering occlusions */ + box-shadow: var(--global-box-shadow); + pointer-events: none; /* Disables click tracking intercept rules so click requests fall directly through to card link triggers */ + animation: 2s infinite pulse; /* Maps cyclical infinite engine loop running the custom scale transformation timing array */ +} + +/* Core keyframe math array configurations establishing tracking boundaries for pulsing notifications */ +@keyframes pulse { + 0%, 100% { + transform: scale(1); /* Neutral resting scale dimensions benchmark configuration */ + } + 50% { + transform: scale(1.05); /* Scales badge canvas layout slightly outwards by 5% at exactly the halfway runtime cycle node */ + } +} diff --git a/src/components/Column/index.jsx b/src/components/Column/index.jsx index b489ef7..3502d00 100644 --- a/src/components/Column/index.jsx +++ b/src/components/Column/index.jsx @@ -9,7 +9,7 @@ */ import React from "react"; -import clsx from "clsx"; +import clsx from "clsx"; // Utility engine for conditionally joining dynamic strings and CSS classes together /** * Renders a standard layout grid column container. @@ -23,8 +23,9 @@ import clsx from "clsx"; */ export default function Column({ children, className, style }) { return ( + // Combines the base structural 'col' styling rule with custom runtime classes passed via props
- {children} + {children} {/* Injects child components or text nodes into the rendered column framework */}
); } diff --git a/src/components/Columns/index.jsx b/src/components/Columns/index.jsx index f1b8ba8..c3e05f5 100644 --- a/src/components/Columns/index.jsx +++ b/src/components/Columns/index.jsx @@ -9,7 +9,7 @@ */ import React from "react"; -import clsx from "clsx"; +import clsx from "clsx"; // Utility engine used for conditionally joining dynamic string classes together /** * Renders a centered grid container and row wrapper for dynamic columns. @@ -23,10 +23,11 @@ import clsx from "clsx"; */ export default function Columns({ children, className, style }) { return ( - // This section encompasses the columns that we will integrate with children from a dedicated component to allow the addition of columns as needed + // Outer responsive grid framework wrapper centering the entire layout section block horizontally on the page canvas
+ {/* Creates the horizontal layout row, combining standard grid properties with dynamic custom styles and classes */}
- {children} + {children} {/* Injects and mounts nested child column components inside the row structure */}
); diff --git a/src/components/CsvTable/index.jsx b/src/components/CsvTable/index.jsx index f09558f..9a39327 100644 --- a/src/components/CsvTable/index.jsx +++ b/src/components/CsvTable/index.jsx @@ -17,15 +17,17 @@ import { parse } from 'csv-parse/browser/esm'; * @returns {JSX.Element} A loading block indicator, a red error notice message, or the completed data grid. */ export default function CsvTable({ csvUrl }) { - const [data, setData] = useState([]); - const [headers, setHeaders] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); + // --- React State Hook Definitions --- + const [data, setData] = useState([]); // Allocation grid matrix holding array collection of parsed data row records + const [headers, setHeaders] = useState([]); // String array container mapping the extracted CSV table column headers + const [loading, setLoading] = useState(true); // Sets initial true visibility condition lock for async background operations + const [error, setError] = useState(null); // Null pointer block caching processing exceptions or networking errors useEffect(() => { // Execution Guard: Don't execute on the server side during Docusaurus build cycles if (typeof window === 'undefined') return; + // Asynchronous background function handling web asset retrieval pipelines async function fetchAndParseCsv() { try { // 1. Fetch the raw asset from the static folder @@ -33,7 +35,7 @@ export default function CsvTable({ csvUrl }) { if (!response.ok) { throw new Error(`HTTP network error! Status: ${response.status}`); } - const csvText = await response.text(); + const csvText = await response.text(); // Unpacks stream response payload into raw unformatted text layout strings // 2. Parse the text using the native ESM module configuration parse( @@ -43,9 +45,10 @@ export default function CsvTable({ csvUrl }) { skip_empty_lines: true, // Bypasses tailing blank spacing rows trim: true // Cuts accidental whitespaces off strings }, + // Callback execution routine fired when parsing processing ends (err, records) => { if (err) { - setError(err.message); + setError(err.message); // Intercepts parsing formatting discrepancies setLoading(false); return; } @@ -53,29 +56,32 @@ export default function CsvTable({ csvUrl }) { if (records && records.length > 0) { // Extract the column header keys from the first record object setHeaders(Object.keys(records[0])); - setData(records); + setData(records); // Stashes records array matrix within local React component hook storage } - setLoading(false); + setLoading(false); // Drops loading flag state to authorize grid layout visibility transitions } ); } catch (err) { - setError(err.message); + setError(err.message); // Intercepts network fetching anomalies setLoading(false); } } - fetchAndParseCsv(); - }, [csvUrl]); + fetchAndParseCsv(); // Triggers structural data retrieval function sequence + }, [csvUrl]); // Re-runs execution loop if target asset URL address maps to new coordinates + // --- Conditional UI Render Guards --- if (loading) return

⏳ Loading data table...

; if (error) return

❌ Error parsing data layout: {error}

; if (data.length === 0) return

⚠️ No data discovered inside the requested file mapping.

; return ( + // Infuses Docusaurus Infima markdown class configurations to style typography layouts beautifully
+ {/* Iterates through the isolated headers array to build table column header blocks */} {headers.map((header) => ( + {/* Iterates over the top-level array collection rows */} {data.map((row, rowIndex) => ( + {/* Nested iterator extracting individual key mapping values for every cell's cross point data field */} {headers.map((header) => ( ))} diff --git a/src/components/HeroCarousel/index.jsx b/src/components/HeroCarousel/index.jsx index 25e533e..1860c3b 100644 --- a/src/components/HeroCarousel/index.jsx +++ b/src/components/HeroCarousel/index.jsx @@ -13,13 +13,13 @@ */ import React from "react"; -import Slider from "react-slick"; -import clsx from "clsx"; -import Link from "@docusaurus/Link"; -import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; +import Slider from "react-slick"; // Import the core react-slick sliding carousel framework layout component +import clsx from "clsx"; // Utility engine used for conditionally joining dynamic string classes together +import Link from "@docusaurus/Link"; // Docusaurus optimized router link component to prevent full-page reloads +import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; // React hook providing access to global site configuration variables -import Heading from "@theme/Heading"; -import styles from "./index.module.css"; +import Heading from "@theme/Heading"; // Swappable theme heading component supporting semantic HTML structures +import styles from "./index.module.css"; // Scoped CSS Modules styling sheet for this specific layout component /** * Renders the text overlay for the hero section, including the site title, @@ -30,14 +30,20 @@ import styles from "./index.module.css"; * @returns {React.JSX.Element} The branded content overlay block. */ function HeroText() { + // Destructures the siteConfig configuration payload from the central Docusaurus context provider const { siteConfig } = useDocusaurusContext(); return ( + // Inner typography container absolute-positioned over the active visual image layer
+ {/* Renders a semantic h1 block using the official site title from docusaurus.config.js */} {siteConfig.title} + {/* Renders the official site subtitle/tagline text from docusaurus.config.js */}

{siteConfig.tagline}

+ {/* Action button grouping container wrapper */}
+ {/* Large, high-contrast call-to-action button routing users directly to the recruitment form page */} Join Us @@ -54,30 +60,35 @@ function HeroText() { * @returns {React.JSX.Element} An autoplaying slideshow with interactive navigation overlays. */ export default function HeroCarousel() { + // Configuration options object passed to configure the underlying react-slick engine const settings = { - dots: true, - infinite: true, - fade: true, - speed: 1000, - slidesToShow: 1, - slidesToScroll: 1, - autoplay: true, - waitForAnimate: false, - arrows: false, + dots: true, // Enables the navigation dots tracker buttons at the bottom of the card frame + infinite: true, // Loops the carousel slides infinitely back to slide 1 upon reaching the terminal item + fade: true, // Deploys a smooth opacity cross-fade transition instead of a horizontal slide swipe action + speed: 1000, // Setting tracking animation cross-fade layout transition durations (1000ms = 1 second) + slidesToShow: 1, // Dictates the volume of slides exposed on screen simultaneously within the viewpoint window + slidesToScroll: 1, // Dictates the index increment stepping count value advanced on every progression trigger + autoplay: true, // Automates background image cycling processes without demanding user interaction clicks + waitForAnimate: false, // Disables animation queues to allow immediate navigation interactions during active fades + arrows: false, // Suppresses the native left/right side navigation arrow buttons to clean up visual clutter }; return ( + // Mounts the Slider wrapper, spreading configuration rules and infusing standard Infima hero layout classes + {/* --- CAROUSEL SLIDE ITEM 1 --- */}
- + {/* Re-injects the dynamic text overlay structure directly over the active slide index */} Hero slide showing Scouts in action
+ {/* --- CAROUSEL SLIDE ITEM 2 --- */}
Hero slide showing Scouts in action
+ {/* --- CAROUSEL SLIDE ITEM 3 --- */}
Hero slide showing Scouts in action diff --git a/src/components/HeroCarousel/index.module.css b/src/components/HeroCarousel/index.module.css index fe9fb69..acfb880 100644 --- a/src/components/HeroCarousel/index.module.css +++ b/src/components/HeroCarousel/index.module.css @@ -1 +1,18 @@ -@media screen and (max-width:996px){.heroBanner{padding:0}}.buttons{display:flex;align-items:center;justify-content:center} \ No newline at end of file +/* ============================================================================== + CSS Module Stylesheet: Hero Carousel Typography Overlays + Controls layout structures for action elements and responsive mobile boundaries. + ============================================================================== */ + +/* Responsive media query gate handling layout shifts for smaller viewports */ +@media screen and (max-width: 996px) { + .heroBanner { + padding: 0; /* Strips standard Infima hero padding on tablet and mobile screens to expand the slider full-width */ + } +} + +/* Call-to-action button alignment layout panel box container */ +.buttons { + display: flex; /* Invokes Flexbox layout properties to easily align action triggers side-by-side */ + align-items: center; /* Centers button elements perfectly along vertical coordinate balancing nodes */ + justify-content: center; /* Centers button elements perfectly along the horizontal alignment tracking grid */ +} \ No newline at end of file diff --git a/src/components/HomepageFeatures/index.jsx b/src/components/HomepageFeatures/index.jsx index 8ad142d..96d258b 100644 --- a/src/components/HomepageFeatures/index.jsx +++ b/src/components/HomepageFeatures/index.jsx @@ -11,9 +11,9 @@ */ import clsx from "clsx"; -import styles from "./styles.module.css"; -import Link from "@docusaurus/Link"; -import Heading from "@theme/Heading"; +import styles from "./styles.module.css"; // Scoped CSS Modules styling sheet managing custom cards and hover states +import Link from "@docusaurus/Link"; // Docusaurus optimized internal link router to prevent page refreshes +import Heading from "@theme/Heading"; // Structural theme heading component enforcing standard semantic HTML tags /** * Core dataset representing individual scouting unit details. @@ -22,8 +22,9 @@ import Heading from "@theme/Heading"; const FeatureList = [ { title: "Troop 303", + // Webpack resolves this image path from the static directory at compilation build time Jpg: require("@site/static/img/feature-cards/troop303.jpg").default, - UnitSite: "/troop-303", + UnitSite: "/troop-303", // Target destination route for the Boys/Girls Troop page description: ( <> Serving young men ages 11–17 on their journey to Eagle Scout and beyond. @@ -32,8 +33,9 @@ const FeatureList = [ }, { title: "Troop 331", + // Resolves and caches the file asset bundle during the deployment optimization process Jpg: require("@site/static/img/feature-cards/troop331.jpg").default, - UnitSite: "/troop-331", + UnitSite: "/troop-331", // Target destination route for the All-Girl Troop page description: ( <> Providing adventure, leadership, and service opportunities for girls ages 11–17. @@ -42,8 +44,9 @@ const FeatureList = [ }, { title: "Crew 303", + // Resolves the high-adventure co-ed branch image reference location block Jpg: require("@site/static/img/feature-cards/crew303.jpg").default, - UnitSite: "/crew-303", + UnitSite: "/crew-303", // Target destination route for the Venturing Crew page description: ( <> High adventure, leadership, and service opportunities for young men and women ages 14–20. @@ -52,8 +55,9 @@ const FeatureList = [ }, { title: "Pack 303", + // Resolves the entry-level elementary school program image path resource Jpg: require("@site/static/img/feature-cards/pack303.jpg").default, - UnitSite: "/pack-303", + UnitSite: "/pack-303", // Target destination route for the Cub Scout Pack page description: ( <> Starting the journey of Scouting with fun and adventure for boys and girls in grades K–5. @@ -77,12 +81,18 @@ const FeatureList = [ */ function Feature({ Jpg, UnitSite, title, description }) { return ( + // Allocates exactly 3 out of 12 grid spaces per element, making a clean 4-column layout layout grid
+ {/* Link container wrapping the entire card graphic canvas to handle click routing events */} + {/* Background display banner image asset representing the unit */} {title} + {/* Animated visual display box absolute-positioned directly over the image surface */}
+ {/* Semantic h3 header component block displaying the current active unit string */} {title} + {/* Paragraph block summarizing the membership parameters and unit goals */}

{description}

@@ -101,14 +111,19 @@ export default function HomepageFeatures() { return ( <> {" "} + {/* Centered layout row segment initializing section text header indicators */}
Scouting Units
+ {/* Root section context viewport element utilizing module styles layouts */}
+ {/* Fixed horizontal margin spacing box aligning content frames with global layout layouts */}
+ {/* Standard row flex design structure wrapping column grid elements safely */}
+ {/* Loops over the FeatureList matrix records to dynamically inject custom cards onto the page DOM */} {FeatureList.map((props, idx) => ( - + // Spreads data properties object keys and passes unique index numbers for React diff tracking ))}
diff --git a/src/components/HomepageFeatures/styles.module.css b/src/components/HomepageFeatures/styles.module.css index 1bc647c..049eb00 100644 --- a/src/components/HomepageFeatures/styles.module.css +++ b/src/components/HomepageFeatures/styles.module.css @@ -1 +1,108 @@ -.features{padding-bottom:10px}.imageContainer{position:relative;display:inline-block;overflow:hidden;border-radius:8px;background-color:var(--ifm-color-black);height:200px;width:200px}.featureJpg{height:100%;width:100%;object-fit:cover;transition:transform .4s,opacity .4s;display:block}.overlayContent{position:absolute;top:0;left:0;width:100%;height:100%;display:flex;flex-direction:column;justify-content:center;align-items:center;padding:15px;z-index:2}.overlayDescription,.overlayHeading{color:var(--ifm-color-white);text-align:center;z-index:3;margin:0;position:absolute}.overlayHeading{bottom:5px;left:0;right:0;text-shadow:2px 2px 4px rgba(0,0,0,.8);transition:bottom .4s,transform .4s}.overlayDescription{top:50%;left:50%;transform:translate(-50%,-50%);width:90%;opacity:0;transition:opacity .4s;font-size:.85rem;line-height:1.4}.imageContainer:hover .featureJpg{transform:scale(1.1);opacity:.5}.imageContainer:hover .overlayHeading{bottom:75%}.imageContainer:hover .overlayDescription{opacity:1}.imageContainer::after{content:'';position:absolute;inset:0;background-color:rgba(0,0,0,.2);transition:background-color .4s;z-index:1}.imageContainer:hover::after{background-color:rgba(0,0,0,.6)} \ No newline at end of file +/* ============================================================================== + CSS Module Stylesheet: Scouting Unit Grid Feature Cards + Handles responsive spacing, image masking layers, and hover slide-up motions. + ============================================================================== */ + +/* Core workspace block wrapping the entire features collection section layout */ +.features { + padding-bottom: 10px; /* Subtle clearance buffer below the unit section canvas block */ +} + +/* Structural bounding anchor cell wrapping the image graphics and content overlays */ +.imageContainer { + position: relative; /* Establishes absolute positioning boundary context for child content overlays */ + display: inline-block; /* Shrinks bounds tightly around content dimensions preventing width leakage loops */ + overflow: hidden; /* Clips background photos and scaling animations safely within the border radius */ + border-radius: 8px; /* Modern rounded corner radius geometry matching global card layouts */ + background-color: var(--ifm-color-black); /* Enforces dark background underlays to boost image opacity blend styles */ + height: 200px; /* Sets hard locked pixel dimensions creating square card geometry grids */ + width: 200px; +} + +/* Background scouting unit feature presentation card illustration properties */ +.featureJpg { + height: 100%; + width: 100%; + object-fit: cover; /* Scales and center-crops photo frames safely without distorting core aspect ratios */ + transition: transform .4s, opacity .4s; /* Synchronized hardware-accelerated transition curves handling user animations */ + display: block; /* Suppresses inline baseline tracking spaces underneath the image container canvas */ +} + +/* Absolute layout container block overlaying typography directly on top of the image canvas */ +.overlayContent { + position: absolute; /* Unlocks placement tracking values independent of standard document flow schemas */ + top: 0; + left: 0; + width: 100%; + height: 100%; + display: flex; /* Enforces Flexbox layouts to balance layout layers along centralized paths */ + flex-direction: column; + justify-content: center; /* Groups layout layers along the primary vertical directory stacking axis */ + align-items: center; /* Centers layout layers along horizontal screen space alignment coordinates */ + padding: 15px; /* Internal boundary spacing buffer preventing typography clips on small screens */ + z-index: 2; /* Lifts core layout layer up above background photos and darkening overlay panels */ +} + +/* Shared typography alignment parameters targeting unit headings and descriptions simultaneously */ +.overlayDescription, +.overlayHeading { + color: var(--ifm-color-white); /* Enforces a pure high-contrast text color palette rule across layout spaces */ + text-align: center; /* Centers multi-line text blocks across screen layouts */ + z-index: 3; /* Keeps text properties explicitly stacked over dynamic asset overlay surfaces */ + margin: 0; /* Clears default paragraph block text boundaries to maintain clean calculation nodes */ + position: absolute; /* Allows individual elements to slide independently during runtime interactions */ +} + +/* Unit identity tag heading configuration layout block parameters */ +.overlayHeading { + bottom: 5px; /* Initial resting anchor placement pinned right against the card baseline edge floor */ + left: 0; + right: 0; + text-shadow: 2px 2px 4px rgba(0, 0, 0, .8); /* High-density dark text dropshadow ensuring text visibility on bright image surfaces */ + transition: bottom .4s, transform .4s; /* Fluid transformation timers monitoring upward positioning drift speeds */ +} + +/* Narrative description summary paragraph details typography element block */ +.overlayDescription { + top: 50%; /* Centers block vertical alignments inside the container grid space */ + left: 50%; + transform: translate(-50%, -50%); /* Employs precise offset translations to achieve true pixel alignment balance */ + width: 90%; /* Retains thin margins along left/right coordinate walls to protect layout padding */ + opacity: 0; /* Defaults visibility to zero to keep text hidden on initial load states */ + transition: opacity .4s; /* Fades paragraph lines in smoothly when interactive hover events occur */ + font-size: .85rem; + line-height: 1.4; /* Extends spatial row spacing to optimize small-text readability scores */ +} + +/* --- INTERACTIVE HOVER MOTION TRIGGERS --- */ + +/* Zooms background photos inward and drops opacity values to reveal dark underlays */ +.imageContainer:hover .featureJpg { + transform: scale(1.1); /* Magnifies illustration scaling dimensions outwards by exactly 10% */ + opacity: .5; /* Dims image density values by 50% to optimize dark backdrop contrast levels */ +} + +/* Slides unit title headings cleanly upward from the baseline footer track to make room for descriptions */ +.imageContainer:hover .overlayHeading { + bottom: 75%; /* Pulls heading title string lines up into the top vertical quadrant block */ +} + +/* Transitions summary descriptions from completely hidden fields into fully legible content layouts */ +.imageContainer:hover .overlayDescription { + opacity: 1; /* Sets text opacity values to absolute visibility standard rules */ +} + +/* Dynamic CSS pseudo-element dark glass panel overlay covering background elements */ +.imageContainer::after { + content: ''; /* Injects an empty layout canvas layer block into the document node list */ + position: absolute; + inset: 0; /* Sets full-coverage constraints pinning top, bottom, left, and right coordinates to zero */ + background-color: rgba(0, 0, 0, .2); /* Soft subtle darkening shading layer to gently dim raw illustration file colors */ + transition: background-color .4s; /* Handles smooth color transition pacing during interactions */ + z-index: 1; /* Layered explicitly underneath typography objects to protect visibility tracks */ +} + +/* Deepens dark backdrop glass values when users target the card workspace to enforce absolute text readability */ +.imageContainer:hover::after { + background-color: rgba(0, 0, 0, .6); /* Scales mask opacity up to a deep 60% tint layout overlay rule */ +} diff --git a/src/components/Map/index.jsx b/src/components/Map/index.jsx index 804bad0..094fbf5 100644 --- a/src/components/Map/index.jsx +++ b/src/components/Map/index.jsx @@ -13,13 +13,13 @@ */ import React from "react"; -import BrowserOnly from "@docusaurus/BrowserOnly"; -import troop_icon from "/img/map-marker/troop-marker.png"; -import cub_icon from "/img/map-marker/cub-marker.png"; -import Heading from "@theme/Heading"; +import BrowserOnly from "@docusaurus/BrowserOnly"; // Docusaurus isolation utility that prevents code from executing on Node.js servers +import troop_icon from "/img/map-marker/troop-marker.png"; // Custom map pin illustration graphic path for Boy/Girl Troops +import cub_icon from "/img/map-marker/cub-marker.png"; // Custom map pin illustration graphic path for Cub Scout Packs +import Heading from "@theme/Heading"; // Structural theme heading component enforcing standard semantic HTML tags /** - * Generates the map canvas engine, pins geolocation coordinate items, and configurations + * Generates the map canvas engine, pins geolocation coordinate items, and configures * custom PNG image sizes for mapping markers. Runs explicitly in client context. * * @component @@ -29,51 +29,62 @@ import Heading from "@theme/Heading"; * @returns {React.JSX.Element} Leaflet container node engine matching the assigned dimensions. */ function Map() { + // CRITICAL: Require mapping dependencies inline inside this client-only context. + // Standard top-level ES6 imports would crash Node.js at build time because Leaflet demands a browser 'window' object. const L = require("leaflet"); const { MapContainer, TileLayer, Marker, Popup } = require("react-leaflet"); - require("leaflet/dist/leaflet.css"); + require("leaflet/dist/leaflet.css"); // Imports Leaflet's mandatory core structural CSS layout rules + // Configures sizing matrices for the dynamic Troop map marker vector pin graphics const troopIcon = new L.Icon({ iconUrl: troop_icon, - iconSize: [46, 70], // width, height in pixels - iconAnchor: [23, 70], // point of icon that corresponds to marker's location - popupAnchor: [0, -70], // point from which popup should open relative to iconAnchor + iconSize:, // Width and height bounding dimensions of the target icon file in pixels + iconAnchor:, // The specific pixel location node [X, Y] aligned directly over the geographic coordinate point + popupAnchor: [0, -70], // Coordinate offset calculation determining where information cards pop open relative to the anchor }); + // Configures sizing matrices for the dynamic Cub Scout map marker vector pin graphics const cubIcon = new L.Icon({ iconUrl: cub_icon, - iconSize: [46, 70], // width, height in pixels - iconAnchor: [23, 70], // point of icon that corresponds to marker's location - popupAnchor: [0, -70], // point from which popup should open relative to iconAnchor + iconSize:, + iconAnchor:, + popupAnchor: [0, -70], }); return ( + // Base layout node initializing map canvas center view calculations and default resolution tracking zoom multipliers + {/* Fetches and renders OpenStreetMap's geographical background terrain illustration tiles dataset */} - - + + {/* --- MARKER 1: American Legion Post 331 (Troop Meeting Site) --- */} + + {/* Floating display tooltip card opening on user interaction paths */} American Legion Post 331 + {/* Universal high-contrast direction routing query mapping string targeting native tracking apps */} + target="_blank" // Spawns links within completely clean browser subwindows + rel="noopener noreferrer"> // Security parameters masking data leakage transfers across foreign servers Get Directions + + {/* --- MARKER 2: Eagle Elementary School (Pack Meeting Site) --- */} Eagle Elementary School Get Directions @@ -92,7 +103,9 @@ function Map() { */ export default function MapWrapper() { return ( + // Replaces the element during build steps with a static text block until browser engines activate execution pipelines Loading Map...
}> + {/* Lazy-mounts the complete map framework only inside active web browser clients */} {() => } ); diff --git a/src/components/MeetingLocations/index.jsx b/src/components/MeetingLocations/index.jsx index 1535721..2187703 100644 --- a/src/components/MeetingLocations/index.jsx +++ b/src/components/MeetingLocations/index.jsx @@ -11,9 +11,9 @@ */ import React from "react"; -import PackPng from "@site/static/img/logos/pack-icon.png"; -import TroopsPng from "@site/static/img/logos/troop-icon.png"; -import MapWrapper from "@site/src/components/Map"; +import PackPng from "@site/static/img/logos/pack-icon.png"; // Local asset compilation path for the Cub Scout branding icon +import TroopsPng from "@site/static/img/logos/troop-icon.png"; // Local asset compilation path for the Scouts BSA branding icon +import MapWrapper from "@site/src/components/Map"; // Imports the client-side safe isolated interactive map canvas frame /** * Renders an informational split layout section for organization meeting details and maps. @@ -23,31 +23,41 @@ import MapWrapper from "@site/src/components/Map"; */ export default function MeetingLocations() { return ( + // Top-level Flexbox grid dashboard layout workspace bounding container
- {/* Left Side: Clean Visual Information Cards */} + {/* ------------------------------------------------------------------- + LEFT SIDE: Clean Visual Information Cards Panel + ------------------------------------------------------------------- */} + {/* Allocates a flexible base width of 450px; stacks information cards into a tight vertical layout */}
- {/* Card 1: Pack 303 */} + {/* --- CARD 1: Cub Scout Pack 303 Schedule Info Block --- */}
+ {/* Section heading displaying branding colors, using inline flex layout properties to align vector icon graphics */}

Pack Icon Cub Scout Pack 303 (Grades K-5)

+ {/* Weekly calendar schedule entry parameters block */}

When: Tuesdays (School Year) | 6:45 PM – 7:45 PM

+ {/* Geographic baseline physical location description typography parameter lines */}

📍 Eagle Elementary School
555 Sycamore St, Brownsburg, IN 46112

- {/* Card 2: Troops 303 & 331 */} + {/* --- CARD 2: Troops 303 & 331 Schedule Info Block --- */}
+ {/* Section heading displaying Scouts BSA olive branding theme colors across localized layouts */}

Troop Icon Troops 303 (Boys) & 331 (Girls) (Ages 11-17)

+ {/* Year-round calendar schedule entry parameters block */}

When: Every Tuesday (Year-Round) | 6:30 PM – 8:00 PM

+ {/* Geographic baseline physical location description typography parameter lines */}

📍 American Legion Post 331
636 E Main St, Brownsburg, IN 46112 @@ -56,9 +66,12 @@ export default function MeetingLocations() {

- {/* Right Side: Interactive Map */} + {/* ------------------------------------------------------------------- + RIGHT SIDE: Client-Safe Interactive Map Canvas Component + ------------------------------------------------------------------- */} + {/* Allocates a flexible base width of 350px; clips map dimensions inside uniform borders and shadows */}
- + {/* Instantiates the live client-facing geolocation map mapping workspace frame */}
diff --git a/src/components/PhotoAlbumGallery/index.jsx b/src/components/PhotoAlbumGallery/index.jsx index 58262d6..e03948c 100644 --- a/src/components/PhotoAlbumGallery/index.jsx +++ b/src/components/PhotoAlbumGallery/index.jsx @@ -20,6 +20,7 @@ import Counter from "yet-another-react-lightbox/plugins/counter"; import Thumbnails from "yet-another-react-lightbox/plugins/thumbnails"; import Fullscreen from "yet-another-react-lightbox/plugins/fullscreen"; +// Import mandatory structural vendor stylesheets to render album columns and media players accurately import 'react-photo-album/styles.css'; import 'yet-another-react-lightbox/styles.css'; import "yet-another-react-lightbox/plugins/counter.css"; @@ -38,67 +39,78 @@ import "yet-another-react-lightbox/plugins/thumbnails.css"; * */ export default function PhotoAlbumGallery({ context }) { - const [index, setIndex] = useState(-1); - const [photos, setPhotos] = useState([]); + // --- React State Hook Definitions --- + const [index, setIndex] = useState(-1); // Active slider slide tracking pointer (-1 indicates the lightbox is currently closed) + const [photos, setPhotos] = useState([]); // Storage matrix holding array objects of image data (src, width, height, alt) useEffect(() => { + // 1. Map out raw files from the passed Webpack require.context asset bundle map const files = context.keys().map((key) => ({ - src: context(key).default, - alt: key.replace('./', ''), + src: context(key).default, // Extracts compiled production-ready hashed public asset URL strings + alt: key.replace('./', ''), // Normalizes filenames by clearing baseline relative path markers })); + // 2. Wrap each image loader process inside an async Promise block to calculate dimensions safely const loadDimensions = files.map((file) => { return new Promise((resolve) => { - const img = new Image(); - img.src = file.src; + const img = new Image(); // Spawns an unmounted HTMLImageElement memory thread execution context + img.src = file.src; // Initiates background file loading processes over network threads + + // Success execution hook fired immediately after file bytes finish loading on the server img.onload = () => { resolve({ src: file.src, - width: img.naturalWidth || 4, - height: img.naturalHeight || 3, + width: img.naturalWidth || 4, // Extracts pure physical pixel width properties (falls back to a 4:3 default index if zero) + height: img.naturalHeight || 3, // Extracts pure physical pixel height properties alt: file.alt, }); }; + + // Fail-safe tracking exception gate handling missing or broken image assets img.onerror = () => { - resolve({ src: file.src, width: 4, height: 3, alt: file.alt }); + resolve({ src: file.src, width: 4, height: 3, alt: file.alt }); // Resolves default 4:3 boxes to avoid breaking masonry layout algorithms }; }); }); + // 3. Complete all concurrent dimension calculations before updating the state hook matrix data layout Promise.all(loadDimensions).then((resolvedPhotos) => { - setPhotos(resolvedPhotos); + setPhotos(resolvedPhotos); // Overrides local state array data, driving visual layout updates }); - }, [context]); + }, [context]); // Triggers execution pass adjustments if context directory paths change + // --- Conditional UI Render Guard --- if (photos.length === 0) { return

Scanning folder assets...

; } return ( <> + {/* Dynamic layout engine arranging photo blocks flush without uneven rows gap drops */} { - if (containerWidth < 400) return 4; - if (containerWidth < 800) return 5; - return 6; + // Dynamic layout calculation tracking screen grid widths to adjust responsive columns scaling maps + if (containerWidth < 400) return 4; // Tiny screen layouts / mobile panels + if (containerWidth < 800) return 5; // Mid-scale screens / tablets + return 6; // Desktop display interfaces }} - onClick={({ index }) => setIndex(index)} + onClick={({ index }) => setIndex(index)} // Updates the active state index to pop open the corresponding lightbox slide /> + {/* Fullscreen modal media slider component block overlapping global page contexts */} = 0} - index={index} - close={() => setIndex(-1)} - plugins={[Thumbnails, Counter, Fullscreen]} + slides={photos} // Maps target slides asset registry list directly to slide items + open={index >= 0} // Open parameter visibility gate checking if index tracker is awake + index={index} // Set core presentation frame view index focus node + close={() => setIndex(-1)} // Deactivates visibility flags completely on close event execution triggers + plugins={[Thumbnails, Counter, Fullscreen]} // Mounts secondary core navigation plugin utilities modules layers thumbnails={{ - position: "bottom", // Places the carousel row under the main picture - showToggle: false, // Disables and removes the user hide/show button + position: "bottom", // Places the carousel row under the main picture frame container workspace + showToggle: false, // Disables and removes the user hide/show icon button layout elements entirely }} /> ); } - diff --git a/src/components/RecruitmentCards/index.jsx b/src/components/RecruitmentCards/index.jsx index 945b1c0..6491bd8 100644 --- a/src/components/RecruitmentCards/index.jsx +++ b/src/components/RecruitmentCards/index.jsx @@ -68,31 +68,34 @@ const units = [ */ function UnitCard({ title, age, link, bgColor, buttonColor, badgeBg, imgSrc, altText}) { return ( + // Binds inline styling configurations to shape the core layout card footprint layout
+ {/* Top Container grouping branding elements together */}
{altText}

{title}

+ {/* Visual badge element highlighting target age brackets and grade levels */}
+ {/* External link registration anchor button */} { - e.currentTarget.style.transform = 'translateY(-2px)'; - e.currentTarget.style.filter = 'brightness(1.08)'; + e.currentTarget.style.transform = 'translateY(-2px)'; // Gives card button a floating lift look + e.currentTarget.style.filter = 'brightness(1.08)'; // Gently brightens button color highlights }} onMouseOut={(e) => { - e.currentTarget.style.transform = 'translateY(0)'; - e.currentTarget.style.filter = 'brightness(1)'; + e.currentTarget.style.transform = 'translateY(0)'; // Restores default translation points + e.currentTarget.style.filter = 'brightness(1)'; // Re-establishes base canvas brightness limits }} > + {/* Programmatic string parsing splitting text strings to slice unit names dynamically */} Join {title.includes('Pack') ? 'Pack 303' : title.split(' ').slice(1).join(' ')}
@@ -145,44 +151,54 @@ function UnitCard({ title, age, link, bgColor, buttonColor, badgeBg, imgSrc, alt */ export default function SignUpCards({ youtubeId }) { return ( - + // Main centering section component limits max workspace boundary width to a clean 1100px grid path
- {/* 1. Video Player Container */} + + {/* 1. Video Player Framing Canvas Container */}
+ {/* Universal aspect ratio hack ensuring frames scaling calculations preserve an absolute 16:9 box matrix format */}
- {/* 2. Unified Grid Header */} + {/* 2. Unified Text Sub-Header Info Container */}

Ready to Register?

Select your unit below to sign up online today.

- {/* 3. Three-Card Flexbox Grid */} -
- {units.map((unit, idx) => ( - + {/* 3. Three-Card Alignment Layout Grid flex-row workspace element */} +
+ {/* Steps across the units dataset map to automatically build and append cards onto the page DOM */} + {units.map((unit, index) => ( + // Spreads configuration attributes and binds index numbers for React diff engine tracking ))}
-
); -} +} \ No newline at end of file diff --git a/src/components/TroopLeadership/index.jsx b/src/components/TroopLeadership/index.jsx index 2ae3a0f..fa915c7 100644 --- a/src/components/TroopLeadership/index.jsx +++ b/src/components/TroopLeadership/index.jsx @@ -9,7 +9,7 @@ * @requires ./styles.module.css */ import React from 'react'; -import styles from './styles.module.css'; +import styles from './styles.module.css'; // Scoped CSS Modules stylesheet managing grid properties, highlights, and fallback filters /** * Structural definition representing data shapes for individual youth Scouts. @@ -51,14 +51,17 @@ import styles from './styles.module.css'; */ export const RoleCard = ({ title, name, description, img, mentoredBy }) => { let rawNameString = ""; + + // Normalizes dynamic input typing variations down into a standard raw lookup text string if (typeof name === 'string') { rawNameString = name; } else if (typeof name === 'object' && name !== null && typeof name.name === 'string') { rawNameString = name.name; } - // Sanitized fallback to catch blank objects, empty strings, and empty arrays + // Sanitized safety gates to flag missing names, empty listings arrays, or structural placeholder objects const hasNoName = !name || rawNameString.trim() === "( Scout)" || (Array.isArray(name) && name.length === 0); + // Evaluates state check flags to instantly discover if the leadership position is unassigned (Open) const isOpen = hasNoName || rawNameString.toLowerCase() === "open position"; /** @@ -67,16 +70,20 @@ export const RoleCard = ({ title, name, description, img, mentoredBy }) => { * @returns {React.JSX.Element} A contextual metadata layer adjusting typography coloring parameters. */ const renderNameContent = () => { + // Condition 1: Position is vacant -> Renders a clean styled warning notification block if (isOpen) { return
Open Position
; } + // Condition 2: Multi-person position assignment input type -> Maps out an ordered roster list block if (Array.isArray(name)) { return (
{name.map((n, idx) => { + // Unpacks items checks to determine if listing array nodes are objects or raw strings let itemText = typeof n === 'object' ? n.name : n; + // Evaluates youth properties to append rank tags (e.g., "(Eagle Scout)") while avoiding "(Scout Scout)" redundancy let rankText = ''; if (typeof n === 'object' && n.rank) { rankText = n.rank === 'Scout' ? ' (Scout)' : ` (${n.rank} Scout)`; @@ -92,6 +99,7 @@ export const RoleCard = ({ title, name, description, img, mentoredBy }) => { ); } + // Condition 3: Single assignment structure wrapped explicitly within a ScoutObject mapping layout if (typeof name === 'object' && name !== null) { let rankText = ''; if (name.rank) { @@ -100,21 +108,26 @@ export const RoleCard = ({ title, name, description, img, mentoredBy }) => { return
{name.name}{rankText}
; } + // Condition 4: Basic string fallback option for standard adult or singular text profiles return
{name}
; }; return ( + // Dynamic class assignment that shifts layout opacity metrics if isOpen evaluates to a true condition flag
+ {/* Structural profile patch illustration or user placeholder portrait graphic */} {title} + {/* Information text layout compartment box container */}

{title}

- {renderNameContent()} + {renderNameContent()} {/* Dynamically computes name layout text structures via helper loops */}

{description}

+ {/* Conditional rendering block overlaying youth leadership-to-adult mentoring tracking trails */} {mentoredBy && (

🤝 Mentored by: {mentoredBy} @@ -135,6 +148,7 @@ export const RoleCard = ({ title, name, description, img, mentoredBy }) => { * @returns {React.JSX.Element} A layout grid container enforcing flexible spacing guidelines. */ export const Grid = ({ children }) => { + // Injects structural flexbox or responsive CSS grid layout styles across child cell bundles return

{children}
; }; @@ -149,32 +163,45 @@ export const Grid = ({ children }) => { * @returns {React.JSX.Element} A separate display cell visualizing localized patrol assets. */ export const PatrolCard = ({ patrol }) => { + // Guard validation parsing checks scanning patrol leader records to flag empty vacancies const plOpen = !patrol.patrolLeader?.name || patrol.patrolLeader.name.trim() === ""; + + // Computes formatting logic to build the Patrol Leader's youth rank string segment const plRank = patrol.patrolLeader?.rank ? (patrol.patrolLeader.rank === 'Scout' ? ' (Scout)' : ` (${patrol.patrolLeader.rank} Scout)`) : ''; + + // Computes formatting logic to build the Troop Guide / Youth Mentor rank string segment const youthMentorRank = patrol.patrolYouthMentor?.rank ? (patrol.patrolYouthMentor.rank === 'Scout' ? ' (Scout)' : ` (${patrol.patrolYouthMentor.rank} Scout)`) : ''; return (
+ {/* Patrol patch asset graphic (e.g., Fox, Eagle, or Wolf icon patch) */} {`${patrol.patrolName} + {/* Inner profile text tracking compartment container wrapper */}

{patrol.patrolName} Patrol

+ + {/* Patrol Leader Display Field: Dynamic text switch highlighting open vacancies in red */}

Patrol Leader:{' '} {plOpen ? "Open Position" : `${patrol.patrolLeader.name}${plRank}`}

+ + {/* Youth Mentor Display Field: Lists Assigned Troop Guide or Senior Youth Advisor */}

Youth Mentor: {patrol.patrolYouthMentor?.name || "None"}{youthMentorRank}

+ + {/* Adult Mentor Display Field: Lists Assigned Assistant Scoutmaster or Patrol Counselor */}

Adult Mentor: {patrol.patrolAdultMentor?.name || "None"}

diff --git a/src/components/TroopLeadership/styles.module.css b/src/components/TroopLeadership/styles.module.css index 1b8915f..b33309e 100644 --- a/src/components/TroopLeadership/styles.module.css +++ b/src/components/TroopLeadership/styles.module.css @@ -1,87 +1,104 @@ +/* ============================================================================== + CSS Module Stylesheet: Leadership & Patrol Roster Cards + Configures a fluid, responsive auto-fitting grid system using custom brand + theming color tokens for text headers, profile images, and vacant roles. + ============================================================================== */ + /* Card Container Grid Layout */ .grid { display: grid; + /* Automatically generates columns that wrap fluidly down to 320px minimum widths, stretching to fill available viewport rows */ grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); - gap: 20px; + gap: 20px; /* Consistent channel margins dividing neighboring cell blocks */ margin-top: 16px; margin-bottom: 32px; } -/* Base Role Card Layout */ +/* Base Role Card Layout Panel */ .roleCard { - background-color: var(--scouting-america-light-tan); - border: 1px solid var(--scouting-america-dark-tan); - border-radius: 12px; - padding: 20px; - box-shadow: var(--global-box-shadow); - display: flex; - gap: 16px; - align-items: flex-start; - transition: transform 0.2s ease, box-shadow 0.2s ease; + background-color: var(--scouting-america-light-tan); /* Soft thematic sand background tone */ + border: 1px solid var(--scouting-america-dark-tan); /* Thin framing outline boundary stroke */ + border-radius: 12px; /* Modern curved panel corner profile geometry */ + padding: 20px; /* Uniform layout cushions separating contents from card frame walls */ + box-shadow: var(--global-box-shadow); /* Injects smooth site-wide elevation depth casting drop-shadow values */ + display: flex; /* Establishes dynamic Flexbox rows to position profile pictures beside text logs */ + gap: 16px; /* Air gap margin spacing separating the image from the text box block */ + align-items: flex-start; /* Vertical alignment lock keeping child elements flush with the card roof line */ + transition: transform 0.2s ease, box-shadow 0.2s ease; /* Fluid hardware-accelerated tracking timings for layout shifts */ } /* Open Position State Modifier */ .roleCardOpen { - background-color: var(--scouting-america-tan); - border: 1px dashed var(--scouting-america-dark-tan); + background-color: var(--scouting-america-tan); /* Shifts backing to a deeper neutral shade to emphasize vacancy */ + border: 1px dashed var(--scouting-america-dark-tan); /* Transitions solid line outlines to an explicit dashed pattern style */ } /* User & Profile Image Styles */ .cardImg { - width: 64px; + width: 64px; /* Strict square aspect bounding box framework dimensions */ height: 64px; - border-radius: 50%; - object-fit: cover; + border-radius: 50%; /* Transforms source square graphics into clean circular profile portrait views */ + object-fit: cover; /* Centers and masks raw files safely to protect human body aspect ratios */ background-color: var(--scouting-america-light-tan); - border: 2px solid var(--scouting-america-dark-tan); - flex-shrink: 0; + border: 2px solid var(--scouting-america-dark-tan); /* Strong framing border stroke encapsulating the portrait circle */ + flex-shrink: 0; /* Layout lock: prevents horizontal text layout pressures from squishing the portrait */ } +/* Open Position Profile Picture Modifier */ .cardImgOpen { - border: 2px dashed var(--scouting-america-dark-tan); + border: 2px dashed var(--scouting-america-dark-tan); /* Convers user placeholder boundaries to dashed paths matching the open state */ } /* Text & Typography Layout Elements */ .cardContent { - flex-grow: 1; + flex-grow: 1; /* Directs the text box compartment to swell outward to absorb all remaining row width spaces */ } +/* Position title heading string configurations */ .titleText { - margin: 0 0 4px 0; + margin: 0 0 4px 0; /* Clears vertical layout padding fields to keep line heights compact */ color: var(--scouting-america-dark-gray); font-size: 1.15rem; - font-weight: 700; - line-height: 1.3; + font-weight: 700; /* Heavy title typography formatting parameter */ + line-height: 1.3; /* Closes leading gaps preventing long headers from drifting across text blocks */ } +/* Roster entity name text layout block line elements */ .nameContainer { font-size: 0.95rem; font-weight: 600; - color: var(--scouting-america-blue); + color: var(--scouting-america-blue); # Deep high-contrast theme blue color tag indicating an active profile name assignment margin: 0 0 2px 0; } +/* Open Position Name Text Modifier */ .nameContainerOpen { - color: var(--scouting-america-red); + color: var(--scouting-america-red); # Shifts typography colors to a prominent attention-grabbing warning red token } +/* Role abstract text description paragraph block layout */ .descText { font-size: 0.875rem; color: var(--scouting-america-dark-gray); - margin: 0 0 12px 0; - line-height: 1.4; + margin: 0 0 12px 0; /* Bottom cushion isolating text summaries away from mentorship metadata tags */ + line-height: 1.4; /* Optimizes sentence line heights to enhance overall paragraph readability values */ } +/* Footnote segment outlining mentor relationships */ .mentorSection { font-size: 0.8rem; color: var(--scouting-america-dark-gray); margin: 0; - border-top: 1px dashed var(--scouting-america-gray); - padding-top: 8px; - font-style: italic; + border-top: 1px dashed var(--scouting-america-gray); # Fine divider separating main summary paragraphs from mentor rows + padding-top: 8px; /* Spacing buffer pushing text below the dashed line axis */ + font-style: italic; /* Slanted editorial character typography configuration */ } -/* Specific Patrol Card Structural Variant */ +/* ============================================================================== + Specific Patrol Card Structural Variant + ============================================================================== */ + +/* Patrol panel card component container block layout */ .patrolCard { border: 1px solid var(--scouting-america-dark-tan); border-radius: 12px; @@ -93,39 +110,45 @@ align-items: flex-start; } +/* Small patch emblem circular element frame formatting */ .patrolBadge { width: 64px; height: 64px; border-radius: 50%; - object-fit: cover; + object-fit: cover; /* Scale locks the emblem patch graphics without clipping critical shape edges */ background-color: var(--scouting-america-light-tan); border: 1px solid var(--scouting-america-dark-tan); - padding: 1px; + padding: 1px; /* Thin inner breathing track between outline borders and patch contents */ flex-shrink: 0; } +/* Section title layout for specific sub-unit patrol groups */ .patrolHeader { margin: 0 0 12px 0; color: var(--scouting-america-dark-gray); - border-bottom: 1px dashed var(--scouting-america-gray); + border-bottom: 1px dashed var(--scouting-america-gray); /* Floor baseline separator strip splitting headers from crew names */ padding-bottom: 4px; } +/* Unified baseline properties handling metadata line outputs inside the card */ .patrolMetaText { - margin: 0 0 6px 0; + margin: 0 0 6px 0; /* Generates short uniform steps to segment name variables lines cleanly */ font-size: 0.9rem; } +/* Terminal meta row entry parameter adjustments to drop trailing space boundaries */ .patrolMetaTextLast { margin: 0; font-size: 0.9rem; } +/* Target highlight override alerting users to vacant patrol leadership gaps */ .openHighlight { - color: var(--scouting-america-red); - font-weight: bold; + color: var(--scouting-america-red); /* Assigns red font color indicators to vacant labels */ + font-weight: bold; /* Enhances character stroke visibility weights */ } +/* Default styling assignment for standard assigned roster entities strings */ .filledText { - color: var(--scouting-america-dark-gray); + color: var(--scouting-america-dark-gray); /* Blends active records to match standard body text outputs smoothly */ } diff --git a/src/components/UpcomingEvents/index.jsx b/src/components/UpcomingEvents/index.jsx index fc4585e..7421d7d 100644 --- a/src/components/UpcomingEvents/index.jsx +++ b/src/components/UpcomingEvents/index.jsx @@ -9,7 +9,7 @@ * @requires @theme/Heading */ -import Heading from "@theme/Heading"; +import Heading from "@theme/Heading"; // Structural theme heading component enforcing standard semantic HTML tags /** * Renders an iframe-based aggregated Google Calendar matching layout container constraints. @@ -20,17 +20,45 @@ import Heading from "@theme/Heading"; */ export default function UpcomingEvents({}) { return ( + // Outer responsive grid framework container centering the layout section block horizontally
+ {/* Centered primary header title using the swappable Infima alignment utility class */} Upcoming Events + + {/* + Embedded Google Calendar Component + Aggregates multiple underlying .ics data feeds into a unified display window. + + URL Parameter Breakdown: + - height=600 / height="600": Establishes locked viewport height allocations in pixels + - wkst=1: Mandates that the calendar grid weeks begin explicitly on Monday + - ctz=America%2FIndiana%2FIndianapolis: Locks timezone coordinates to Eastern Time (US) + - showPrint=0 / showTitle=0 / showTz=0: Cleans UI clutter by disabling print, titles, and timezone text + + Feed Sources (`src` hashes): + - NW1...7sta: Troop 303 Schedule + - en.usa#holiday: Standard United States Public Holidays + - d4v...8rkp: Troop 331 Schedule + - p3u...g97a: Cub Scout Pack 303 Schedule + - rgd...3rea: Venturing Crew 303 Schedule + + Brand Hex Color Palette Maps (`color` codes): + - %23006b3f: Scouts BSA Olive Green (#006b3f) + - %23d6cebd: Scouting America Tan (#d6cebd) + - %23003f87: Cub Scouts Blue (#003f87) + - %23ce1126: Holiday / Alert Red (#ce1126) + - %23fcd116: Cub Scouts Gold (#fcd116) + */}
); } + diff --git a/src/pages/index.jsx b/src/pages/index.jsx index 9fb1335..8e30401 100644 --- a/src/pages/index.jsx +++ b/src/pages/index.jsx @@ -14,12 +14,12 @@ * @requires @site/src/components/UpcomingEvents */ -import Layout from "@theme/Layout"; -import HomepageFeatures from "@site/src/components/HomepageFeatures"; -import UpcomingEvents from "@site/src/components/UpcomingEvents"; -import HeroCarousel from "@site/src/components/HeroCarousel"; -import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; -import HomepageBlogCards from "@site/src/components/BlogCard"; +import Layout from "@theme/Layout"; // Imports the global Docusaurus scaffolding layout (injects top navigation bars, mobile menus, and site footers) +import HomepageFeatures from "@site/src/components/HomepageFeatures"; // Layout component grid highlighting the active units (Troop 303, Pack 303, etc.) +import UpcomingEvents from "@site/src/components/UpcomingEvents"; // Calendar aggregator component mapping out global group schedules +import HeroCarousel from "@site/src/components/HeroCarousel"; // Autoplay picture slideshow component anchoring home recruitment copy +import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; // System configuration context hook fetching variables from docusaurus.config.js +import HomepageBlogCards from "@site/src/components/BlogCard"; // Grid section displaying the most recent adventure posts on the site /** * Renders the master homepage structure wrapped within the global Docusaurus layout framework. @@ -29,18 +29,29 @@ import HomepageBlogCards from "@site/src/components/BlogCard"; * @returns {React.JSX.Element} The fully assembled homepage template. */ export default function Home() { + // Pulls system metadata configurations directly out of your repository's central workspace environment config layer const { siteConfig } = useDocusaurusContext(); return ( + // Wraps everything inside the global frame layout, providing clean search engine metadata mapping arguments + {/* Dynamic top carousel block containing background imagery and main onboarding buttons */} + + {/* Semantic main HTML content workspace block containing standard page subdivisions */}
+ {/* Row block section mapping out grid profiles for each charter scouting unit */} + + {/* Row block section displaying a grid of recent blog entries compiled on the server */} + + {/* Row block section mounting the responsive multi-unit shared schedule overview frame */}
); } + diff --git a/src/theme/Footer/index.jsx b/src/theme/Footer/index.jsx index 33f8d32..cda4d73 100644 --- a/src/theme/Footer/index.jsx +++ b/src/theme/Footer/index.jsx @@ -12,6 +12,21 @@ import React from "react"; import Footer from "@theme-original/Footer"; import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; +/** +/** + * @file index.js + * @description A custom wrapper that extends the default Docusaurus footer layout via swizzling. + * Appends a secondary custom copyright split-section utilizing global site configuration fields. + * + * @module FooterWrapper + * @requires React + * @requires @theme-original/Footer + * @requires @docusaurus/useDocusaurusContext + */ +import React from "react"; +import Footer from "@theme-original/Footer"; // Imports the unswizzled core Docusaurus original theme footer blueprint +import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; // React context hook to fetch metadata arrays out of docusaurus.config.js + /** * Renders the original system footer layout integrated with customized organization compliance details. * @@ -20,17 +35,28 @@ import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; * @returns {React.JSX.Element} The original footer element coupled with custom bottom layout panels. */ export default function FooterWrapper(props) { + // Pulls system config context variables directly out of your repository's central runtime layer const { siteConfig } = useDocusaurusContext(); + + // Destructures the custom string fields variables defined inside your docusaurus.config.js object mapping array const { copyright1, copyright2 } = siteConfig.customFields; return ( <> + {/* Renders the un-swizzled standard Docusaurus original theme links matrix, passing all default props downstream safely */}
{header} @@ -84,10 +90,13 @@ export default function CsvTable({ csvUrl }) {
+ {/* Safety fallback checking field elements for empty null values to display empty spaces instead */} {row[header] !== undefined && row[header] !== null ? row[header] : ""}
- {folderName} {title} System architecture and formatting rules for LLMs contributing to this +> repository. + +## Project Profile + +- **Stack:** Docusaurus v3+, React, MDX, JavaScript/TypeScript, Python. +- **Audience:** Scouting America members, parents, and leaders. +- **Design Core:** Custom theme using Scouting America branding + (Khaki/Blue/Red). +- **Dark Mode:** Explicitly disabled. Code must only support light mode. +- **Source Code:** Hosted at `https://github.com/scouting331/scoutSite`. +- **Hosting & CI/CD:** Built automatically by a worker on commit and deployed to + `https://brownsburgscouts.org`. + +## Organization Profile & Unit Context + +- **Sponsor:** Chartered by American Legion Post 331 in Brownsburg, IN. +- **Core Mission:** Developing tomorrow's leaders through character building, + citizenship training, and personal fitness. +- **Pack 303:** Co-ed Cub Scout Pack for elementary youth (Grades K-5). Focuses + on family camping, foundational leadership skills, and advancement. Meets + weekly during the school year at Eagle Elementary in Brownsburg, IN. +- **Troop 331:** Scouts BSA Girls Troop (Ages 11-17). Focuses on youth-led + leadership and life preparation to build tomorrow's female leaders. The + ultimate achievement for these Scouts is the rank of Eagle. +- **Troop 303:** Scouts BSA Boys Troop (Ages 11-17), known as "The Legendary + Troop 303". Focuses on outdoor adventure and intensive leadership development + to prepare young men for future success. The ultimate achievement for these + Scouts is the rank of Eagle Scout. +- **Crew 303:** Co-ed Venturing Crew for older youth (Ages 14-20). Focuses on + high adventure, high-intensity camping, and advanced youth independence, + continuing the mission of peer leadership. + +## Codebase Map & Router Configurations + +### Content Plugins (Multi-Instance Docs) + +This site uses multiple instances of the Docusaurus docs plugin. + +- `/docs/`: Main instance. Core Scouting unit documents and shared files. Uses + `sidebarsDocs.js`. +- `/cookbook/`: Custom instance. Camping recipes ecosystem. Uses + `sidebarCookbook.js`. + +### Blogs & Components + +- `/blog/`: Unit activity updates. Cross-referenced via `/blog/authors.yml` and + `/blog/tags.yml`. +- `/src/pages/`: Main entry point and unit-specific landing pages. +- `/src/components/[ComponentName]/index.jsx`: Modular UI components. Optional + localized `styles.module.css` inside the same folder. +- `/static/`: Shared media assets (images, PDFs). Reference using absolute paths + (e.g., `/img/logo.png`). +- `/src/css/custom.css`: Target file for global font and CSS variable overrides. + +## Strict Code Conventions + +### Frontmatter Definitions + +AI must generate frontmatter matching these strict schemas: + +#### Docs & Cookbook (`/docs/`, `/cookbook/`) + +```yaml +title: "Page Title" +description: "SEO description under 160 chars" +``` + +#### Blog Posts (`/blog/`) + +```yaml +title: "Post Title" +date: YYYY-MM-DD +authors: [author_key_from_authors_yml] +tags: [unit_tag_from_tags_yml] +``` + +_Allowed tags in `tags.yml`:_ `troop-303`, `troop-331`, `pack-303`, `crew-303`. + +#### MDX Pages (`/src/pages/`) + +```yaml +description: "SEO description" +hide_table_of_contents: true +``` + +### Component & Markup Rules + +- **Formatting:** All narrative content must use `.mdx` extensions. Custom + components must use `.jsx`. +- **Built-ins:** Always default to native ``, ``, and + `` components. +- **Custom HTML:** Never write raw inline HTML in markdown. Wrap HTML into a + reusable component in `/src/components/`. +- **Modification:** Never modify `node_modules`. Use `npm run swizzle` for + layout overrides. + +## CLI Core Commands + +- **Local Dev:** `npm run start` diff --git a/tmp_llms.md b/tmp_llms.md new file mode 100644 index 0000000..9aaa915 --- /dev/null +++ b/tmp_llms.md @@ -0,0 +1,61 @@ +# American Legion Post 331 Scouting Units - Docusaurus Site Context + +> System architecture and formatting rules for LLMs contributing to this repository. + +## Project Profile +- **Stack:** Docusaurus v3+, React, MDX, JavaScript/TypeScript, Python. +- **Audience:** Scouting America members, parents, and leaders. +- **Design Core:** Custom theme using Scouting America branding (Khaki/Blue/Red). +- **Dark Mode:** Explicitly disabled. Code must only support light mode. + +## Codebase Map & Router Configurations + +### Content Plugins (Multi-Instance Docs) +This site uses multiple instances of the Docusaurus docs plugin. +- `/docs/`: Main instance. Core Scouting unit documents and shared files. Uses `sidebarsDocs.js`. +- `/cookbook/`: Custom instance. Camping recipes ecosystem. Uses `sidebarCookbook.js`. + +### Blogs & Components +- `/blog/`: Unit activity updates. Cross-referenced via `/blog/authors.yml` and `/blog/tags.yml`. +- `/src/pages/`: Main entry point and unit-specific landing pages. +- `/src/components/[ComponentName]/index.jsx`: Modular UI components. Optional localized `styles.module.css` inside the same folder. +- `/static/`: Shared media assets (images, PDFs). Reference using absolute paths (e.g., `/img/logo.png`). +- `/src/css/custom.css`: Target file for global font and CSS variable overrides. + +## Strict Code Conventions + +### Frontmatter Definitions +AI must generate frontmatter matching these strict JSON schemas: + +#### Docs & Cookbook (`/docs/`, `/cookbook/`) +```yaml +title: "Page Title" +description: "SEO description under 160 chars" +``` + +#### Blog Posts (`/blog/`) +```yaml +title: "Post Title" +date: YYYY-MM-DD +authors: [author_key_from_authors_yml] +tags: [unit_tag_from_tags_yml] +``` + +#### MDX Pages (`/src/pages/`) +```yaml +description: "SEO description" +hide_table_of_contents: true +``` + +### Component & Markup Rules +- **Formatting:** All narrative content must use `.mdx` extensions. Custom components must use `.jsx`. +- **Built-ins:** Always default to native ``, ``, and `` components. +- **Custom HTML:** Never write raw inline HTML in markdown. Wrap HTML into a reusable component in `/src/components/`. +- **Modification:** Never modify `node_modules`. Use `npm run swizzle` for layout overrides. + +## CLI Core Commands +- **Local Dev:** `npm run start` +- **Code Storage:** The code is stored and version controlled in a Github + repository at https://github.com/scouting331/scoutSite +- **Site Hosting:** After a pull request, a worker builds the site for + hosting on Cloudflare at https://brownsburgscouts.org From 244e1947cbdafac62529a873f2a908b22fef4799 Mon Sep 17 00:00:00 2001 From: shoverbj Date: Thu, 18 Jun 2026 07:22:26 -0400 Subject: [PATCH 34/50] updates to grant blog --- blog/2026-06-17-grant-b.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blog/2026-06-17-grant-b.mdx b/blog/2026-06-17-grant-b.mdx index 5d68867..e59b47e 100644 --- a/blog/2026-06-17-grant-b.mdx +++ b/blog/2026-06-17-grant-b.mdx @@ -1,7 +1,7 @@ --- title: "Congratulations to Grant B" date: 2026-06-17 -authors: [ben-s] +authors: [benjamin-shover] tags: [troop-303] --- From 84defa9a1b8736f0b61f736bc43fcd49a40ee708 Mon Sep 17 00:00:00 2001 From: shoverbj Date: Thu, 18 Jun 2026 08:04:01 -0400 Subject: [PATCH 35/50] consolidate workflows add facebook integration --- .github/scripts/add_author.py | 142 ++++++++++++++ .github/scripts/fb_router.py | 135 +++++++++++++ .github/scripts/generate_docs.py | 3 + .github/workflows/blog-to-facebook.yml | 55 ++++++ .github/workflows/content-management.yml | 221 ++++++++++++++++++++++ .github/workflows/create-blog-post.yml | 66 ------- .github/workflows/create-doc.yml | 69 ------- .github/workflows/process-new-authors.yml | 213 --------------------- 8 files changed, 556 insertions(+), 348 deletions(-) create mode 100644 .github/scripts/add_author.py create mode 100644 .github/scripts/fb_router.py create mode 100644 .github/workflows/blog-to-facebook.yml create mode 100644 .github/workflows/content-management.yml delete mode 100644 .github/workflows/create-blog-post.yml delete mode 100644 .github/workflows/create-doc.yml delete mode 100644 .github/workflows/process-new-authors.yml diff --git a/.github/scripts/add_author.py b/.github/scripts/add_author.py new file mode 100644 index 0000000..baf67ad --- /dev/null +++ b/.github/scripts/add_author.py @@ -0,0 +1,142 @@ +""" +GitHub Action Automation Script for Author Onboarding. + +This script parses author profile details from a GitHub Issue payload, +validates the input fields, ensures author/slug uniqueness against an existing +YAML database, downloads and converts the author's avatar to WebP format, +and appends the new record to the project's central author configuration file. + +Global Configurations: + AUTHORS_FILE (str): Path to the target YAML file where author entries are appended. + TEMPLATE_FILE (str): Path to the GitHub Issue template formatting definition. + AUTHORS_IMG_DIR (str): Destination directory for processed author avatars. + +Environment Dependencies: + ISSUE_DATA: A JSON string passed via GitHub Actions runner containing: + - name (str): The author's full name. + - title (str): The author's professional role or title. + - image_url (str): Markdown-wrapped URL pointing to the user's avatar. +""" + +import json +import yaml +import re +import os +import sys +import urllib.request +from PIL import Image, ImageOps + +# Constants defining project directory structure +AUTHORS_FILE = 'blog/authors.yml' +TEMPLATE_FILE = '.github/ISSUE_TEMPLATE/01-new-blog-post.yml' +AUTHORS_IMG_DIR = 'static/img/blog/authors' + +def main(): + """ + Processes and onboard a new blog author from GitHub Actions issue data. + + This function extracts author metadata from an environment-supplied JSON + string, performs validation, generates a URL-safe unique slug, downloads + the remote avatar, converts it to WebP format, and appends the finalized + profile data to the project's authors YAML registry. + + Raises: + SystemExit (1): If required inputs are missing, or if the author's + name already exists in the registry database. + """ + + # Retrieve issue data passed as a JSON string from the CI/CD environment + issue_json = os.environ.get("ISSUE_DATA", "{}") + data = json.loads(issue_json) + + # Extract and clean author input fields + author_name = data.get("name", "").strip() + author_title = data.get("title", "").strip() + raw_image_url = data.get("image_url", "").strip() + + # Isolate the image URL if it's wrapped in markdown format e.g. (https://url.com) + image_url = "" + url_match = re.search(r'\((https://[^\)]+)\)', raw_image_url) + if url_match: + image_url = url_match.group(1) + + # Halt execution if required fields are missing + if not author_name or not author_title: + print("Missing required fields. Exiting.") + sys.exit(1) + + # Read existing content to check for duplicates without rewriting via YAML loader + raw_content = "" + if os.path.exists(AUTHORS_FILE): + with open(AUTHORS_FILE, 'r', encoding='utf-8') as f: + raw_content = f.read() + + # Simple case-insensitive duplicate name check in existing YAML + if f"name: {author_name}" in raw_content or f'name: "{author_name}"' in raw_content: + print(f"::error::The author name '{author_name}' already exists.") + sys.exit(1) + + # Generate a standard URL-friendly slug from the author's name + slug = author_name.lower() + slug = re.sub(r'[^a-z0-9\s-]', '', slug) + slug = re.sub(r'[\s-]+', '-', slug).strip('-') + + final_slug = slug + counter = 1 + + # Scan file text to ensure slug uniqueness, appending a counter if it collides + while f"{final_slug}:" in raw_content: + final_slug = f"{slug}-{counter}" + counter += 1 + + # Process and download the avatar image if an URL was provided + final_image_path = "" + if image_url: + os.makedirs(AUTHORS_IMG_DIR, exist_ok=True) + + # Remove any query parameters from URL to get file extension + clean_url = image_url.split("?") + _, ext = os.path.splitext(clean_url[0]) + if not ext: + ext = ".jpg" + + # Download image to a temporary file path + tmp_avatar_path = f"/tmp/raw_avatar{ext}" + try: + urllib.request.urlretrieve(image_url, tmp_avatar_path) + + # Format and convert the image to WebP using Pillow + target_file_name = f"{final_slug}.webp" + target_full_path = os.path.join(AUTHORS_IMG_DIR, target_file_name) + + # Open the temporary image, convert, and save as webp + with Image.open(tmp_avatar_path) as img: + img = ImageOps.exif_transpose(img) # Preserve original orientation + img.convert("RGB").save(target_full_path, "webp", quality=80) + + final_image_path = target_full_path + + except Exception as e: + print(f"Warning: Could not process image from {image_url}. Error: {e}") + # Fallback if image processing fails + final_image_path = "" + + # Prepare author object for YAML + new_author_entry = { + final_slug: { + "name": author_name, + "title": author_title, + "image": final_image_path if final_image_path else None + } + } + + # Append the new author YAML block to the end of the authors file + with open(AUTHORS_FILE, 'a', encoding='utf-8') as f: + yaml.dump(new_author_entry, f, default_flow_style=False, allow_unicode=True) + + print(f"Successfully added author '{author_name}' with slug '{final_slug}'.") + + +if __name__ == "__main__": + main() + diff --git a/.github/scripts/fb_router.py b/.github/scripts/fb_router.py new file mode 100644 index 0000000..0ca1577 --- /dev/null +++ b/.github/scripts/fb_router.py @@ -0,0 +1,135 @@ +"""Facebook Routing Automation for Docusaurus Blogs. + +This script parses recently added Docusaurus Markdown files, extracts their +metadata (title, slug, tags), and determines the target Facebook Page +destination based on defined tagging rules. The values are then exported +to the GitHub Actions environment. +""" + +import os +import re +import subprocess +import sys + +def get_last_commit_added_blog_file(): + """Finds the first newly added markdown file in the blog directory. + + Queries git diff for files added in the most recent commit and filters + them to ensure they reside in the Docusaurus 'blog/' folder and have + a markdown extension. + + Returns: + list[str] | None: A list containing file paths of new blog posts, + or None if no matching files are found or an error occurs. + """ + try: + # Run git diff command to locate new files + cmd = ["git", "diff", "--name-only", "--diff-filter=A", "HEAD~1", "HEAD"] + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + files = result.stdout.splitlines() + + # Filter for markdown files in the blog folder + blog_files = [f for f in files if f.startswith("blog/") and f.endswith((".md", ".mdx"))] + return blog_files[0] if blog_files else None + except subprocess.CalledProcessError: + print("Error reading git diff.") + return None + +def parse_front_matter(file_path): + """Extracts title, slug, and tags from Docusaurus front matter. + + Reads the top YAML block of the markdown file using regular expressions. + If a slug is missing, it automatically derives one from the filename + while stripping out standard Docusaurus date prefixes. + + Args: + file_path (str): The relative path to the markdown file. + + Returns: + tuple[str, str, str]: A tuple containing the post title, the URL slug, + and a lowercase block of tag strings for categorization. + """ + with open(file_path, "r", encoding="utf-8") as f: + content = f.read() + + # Match the front matter block enclosed between --- + front_matter_match = re.search(r"^---\s*\n(.*?)\n---", content, re.DOTALL | re.MULTILINE) + if not front_matter_match: + return None, None, "" + + fm_text = front_matter_match.group(1) + + # Extract title + title_match = re.search(r"^title:\s*(.*)$", fm_text, re.MULTILINE) + title = title_match.group(1).strip(" '\"") if title_match else "New Blog Post" + + # Extract slug + slug_match = re.search(r"^slug:\s*(.*)$", fm_text, re.MULTILINE) + if slug_match: + slug = slug_match.group(1).strip(" '\"") + else: + # Fallback to filename tracking if slug is omitted + filename = os.path.basename(file_path) + base_name = os.path.splitext(filename)[0] + slug = re.sub(r"^\d{4}-\d{2}-\d{2}-", "", base_name) # Strip date prefix + + # Extract tags section as a lowercase block for easier routing matching + tags_match = re.search(r"^tags:\s*(.*)$", fm_text, re.MULTILINE) + tags_text = tags_match.group(1).lower() if tags_match else "" + + # Handle multi-line YAML lists for tags if single line wasn't caught cleanly + if not tags_text and "tags:" in fm_text: + tags_text = fm_text.split("tags:")[1].lower() + + return title, slug, tags_text + +def determine_target_page(tags_text): + """Maps tags to specific Facebook page categories. + + Evaluates the parsed tags string against keyword lists to find matches + for specialized Facebook Pages (e.g., TECH or LIFESTYLE). Falls back to + a DEFAULT profile if no conditions match. + + Args: + tags_text (str): A string containing the post's front-matter tags. + + Returns: + str: A string token ("TECH", "LIFESTYLE", or "DEFAULT") indicating + the designated target channel. + """ + tech_keywords = ["tech", "programming", "coding", "developer"] + lifestyle_keywords = ["lifestyle", "travel", "personal"] + + if any(kw in tags_text for kw in tech_keywords): + return "TECH" + elif any(kw in tags_text for kw in lifestyle_keywords): + return "LIFESTYLE" + return "DEFAULT" + +def main(): + """Orchestrates script lifecycle execution. + + Coordinates the discovery, extraction, categorization, and exporting + of blog metadata to the `GITHUB_OUTPUT` file stream for consumption by + downstream GitHub workflow pipeline steps. + """ + new_file = get_last_commit_added_blog_file() + if not new_file: + print("No new blog files found.") + with open(os.environ["GITHUB_OUTPUT"], "a") as go: + go.write("has_new_post=false\n") + sys.exit(0) + + title, slug, tags_text = parse_front_matter(new_file) + target_page = determine_target_page(tags_text) + + # Expose variables to subsequent GitHub Actions steps via GITHUB_OUTPUT + github_output_path = os.environ["GITHUB_OUTPUT"] + with open(github_output_path, "a") as go: + go.write(f"has_new_post=true\n") + go.write(f"title={title}\n") + go.write(f"slug={slug}\n") + go.write(f"target_page={target_page}\n") + +if __name__ == "__main__": + main() diff --git a/.github/scripts/generate_docs.py b/.github/scripts/generate_docs.py index 46c7799..aacc929 100644 --- a/.github/scripts/generate_docs.py +++ b/.github/scripts/generate_docs.py @@ -245,3 +245,6 @@ def main(): doc_file.write(doc_payload) print(f"[Pipeline Complete] Document successfully written to: {markdown_path}") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/.github/workflows/blog-to-facebook.yml b/.github/workflows/blog-to-facebook.yml new file mode 100644 index 0000000..df1bc01 --- /dev/null +++ b/.github/workflows/blog-to-facebook.yml @@ -0,0 +1,55 @@ +name: Route Blog Posts to Facebook Pages + +on: + push: + branches: + - main + paths: + - 'blog/**' + +jobs: + facebook-routing: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 2 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.x' + + - name: Parse Metadata via Python + id: blog_data + run: python .github/scripts/fb_router.py + + - name: Set Facebook Credentials Dynamically + if: steps.blog_data.outputs.has_new_post == 'true' + id: set_tokens + run: | + TARGET="${{ steps.blog_data.outputs.target_page }}" + if [ "$TARGET" = "TECH" ]; then + echo "page_id=${{ secrets.FB_PAGE_ID_TECH }}" >> $GITHUB_OUTPUT + echo "access_token=${{ secrets.FB_ACCESS_TOKEN_TECH }}" >> $GITHUB_OUTPUT + elif [ "$TARGET" = "LIFESTYLE" ]; then + echo "page_id=${{ secrets.FB_PAGE_ID_LIFESTYLE }}" >> $GITHUB_OUTPUT + echo "access_token=${{ secrets.FB_ACCESS_TOKEN_LIFESTYLE }}" >> $GITHUB_OUTPUT + else + echo "page_id=${{ secrets.FB_PAGE_ID_DEFAULT }}" >> $GITHUB_OUTPUT + echo "access_token=${{ secrets.FB_ACCESS_TOKEN_DEFAULT }}" >> $GITHUB_OUTPUT + fi + + - name: Send Link to Selected Facebook Page + if: steps.blog_data.outputs.has_new_post == 'true' + run: | + BLOG_URL="https://brownsburgscouts.org{{ steps.blog_data.outputs.slug }}" + MESSAGE="✍️ New Blog Post: ${{ steps.blog_data.outputs.title }} + + Read the full article here: $BLOG_URL" + + curl -X POST "https://facebook.com{{ steps.set_tokens.outputs.page_id }}/feed" \ + -d "message=$(echo "$MESSAGE")" \ + -d "link=$BLOG_URL" \ + -d "access_token=${{ steps.set_tokens.outputs.access_token }}" diff --git a/.github/workflows/content-management.yml b/.github/workflows/content-management.yml new file mode 100644 index 0000000..0e9329e --- /dev/null +++ b/.github/workflows/content-management.yml @@ -0,0 +1,221 @@ +name: Content Management Automation + +on: + issues: + types: [opened] + +jobs: + # WORKFLOW 1: Generate Blog Post from Issue + create-post: + if: contains(github.event.issue.labels.*.name, 'blog') + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@v6 + + - name: Generate GitHub App Token + id: app-token + uses: actions/create-github-app-token@v3 + with: + app-id: ${{ secrets.APP_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + + - name: Parse Issue Form + id: parse + uses: stefanbuck/github-issue-parser@v3 + with: + template-path: .github/ISSUE_TEMPLATE/01-new-blog-post.yml + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.x" + + - name: Install Dependencies + run: | + python -m pip install --upgrade pip + pip install Pillow python-dateutil + + - name: Process Issue Data and Assets (Python Optimizer) + id: process + env: + ISSUE_JSON: ${{ steps.parse.outputs.jsonString }} + run: | + python .github/scripts/generate_post.py + + - name: Create Pull Request + id: cpr + uses: peter-evans/create-pull-request@v8 + with: + token: ${{ steps.app-token.outputs.token }} + commit-message: "feat(blog): add new post from issue #${{ github.event.issue.number }}" + branch: "automation/issue-${{ github.event.issue.number }}-${{ env.BLOG_FILENAME }}" + title: "feat(blog): ${{ github.event.issue.title }}" + body: | + This Pull Request was automatically generated from Issue #${{ github.event.issue.number }}. + Closes #${{ github.event.issue.number }} + delete-branch: true + labels: | + blog + + - name: Comment on Issue with PR Link + if: steps.cpr.outputs.pull-request-number != '' + uses: peter-evans/create-or-update-comment@v5 + with: + token: ${{ steps.app-token.outputs.token }} + issue-number: ${{ github.event.issue.number }} + body: | + 🎉 Success! A staging branch has been created. + + Your blog post is ready for review here:👉 ${{ steps.cpr.outputs.pull-request-url }} + + # WORKFLOW 2: Generate Document from Issue + create-doc: + if: contains(github.event.issue.labels.*.name, 'docs') + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@v6 + + - name: Generate GitHub App Token + id: app-token + uses: actions/create-github-app-token@v3 + with: + app-id: ${{ secrets.APP_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + + - name: Parse Issue Form + id: parse + uses: stefanbuck/github-issue-parser@v3 + with: + template-path: ./github/ISSUE_TEMPLATE/02-new-document.yml + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.x" + + - name: Install Dependencies + run: | + python -m pip install --upgrade pip + pip install Pillow + + - name: Process Issue Data and Assets (Python Optimizer) + id: process + env: + ISSUE_JSON: ${{ steps.parse.outputs.jsonString }} + run: | + python .github/scripts/generate_docs.py + + - name: Create Pull Request + id: cpr + uses: peter-evans/create-pull-request@v8 + with: + token: ${{ steps.app-token.outputs.token }} + commit-message: "feat(docs): add new doc from issue #${{ github.event.issue.number }}" + branch: "automation/issue-${{ github.event.issue.number }}-${{ env.DOC_FILENAME }}" + title: "feat(docs): ${{ github.event.issue.title }}" + body: | + This Pull Request was automatically generated from Issue #${{ github.event.issue.number }}. + Closes #${{ github.event.issue.number }} + delete-branch: true + labels: | + docs + + - name: Comment on Issue with PR Link + if: steps.cpr.outputs.pull-request-number != '' + uses: peter-evans/create-or-update-comment@v5 + with: + token: ${{ steps.app-token.outputs.token }} + issue-number: ${{ github.event.issue.number }} + body: | + 🎉 Success! A staging branch has been created. + + Your document is ready for review here:👉 ${{ steps.cpr.outputs.pull-request-url }} + + # WORKFLOW 3: Process New Author Request + add-author-and-pr: + if: contains(github.event.issue.labels.*.name, 'new-author') + runs-on: ubuntu-latest + steps: + - name: Checkout Repo + uses: actions/checkout@v6 + + - name: Generate Token + id: app-token + uses: actions/create-github-app-token@v3 + with: + app-id: ${{ secrets.APP_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + + - name: Parse Issue Form + id: parse + uses: stefanbuck/github-issue-parser@v3 + with: + template-path: .github/ISSUE_TEMPLATE/03-new-blog-author.yml + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.x" + + - name: Install Dependencies + run: | + python -m pip install --upgrade pip + pip install Pillow PyYAML python-dateutil + + - name: Process Author and Update Files + id: process_files + env: + ISSUE_DATA: ${{ steps.parse.outputs.jsonString }} + run: | + python .github/scripts/add_author.py + + - name: Create Pull Request + if: success() + uses: peter-evans/create-pull-request@v8 + with: + token: ${{ steps.app-token.outputs.token }} + commit-message: "feat: add new blog author via Issue #${{ github.event.issue.number }}" + title: "feat: add new author from Issue #${{ github.event.issue.number }}" + body: | + This PR automatically handles three tasks: + 1. Downloads, optimizes, and names the avatar image matching the unique author slug. + 2. Adds the new author data mapping properties into `blog/authors.yml`. + 3. Re-generates and sorts the author dropdown options inside the issue templates. + Closes #${{ github.event.issue.number }}. + CODEOWNERS have been automatically assigned to review. + branch: "automation/issue-${{ github.event.issue.number }}" + delete-branch: true + labels: | + new-author + + - name: Comment on Success + if: success() + uses: peter-evans/create-or-update-comment@v5 + with: + token: ${{ steps.app-token.outputs.token }} + issue-number: ${{ github.event.issue.number }} + body: | + Hi there! An automated Pull Request has been generated to add you to the blog authors file and update our submission dropdown tools. The project's CODEOWNERS have been notified to review and merge the changes. Once merged, your author profile and dropdown options will be active! + + - name: Handle Duplicate / Failure + if: failure() + uses: actions/github-script@v9 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const issueNumber = context.issue.number; + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + body: "🛑 **Registration Error:** It looks like an author profile with this name already exists in `blog/authors.yml`. Duplicate entries are not allowed. This issue will now be closed automatically." + }); + await github.rest.issues.update({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + state: 'closed', + state_reason: 'not_planned' + }); diff --git a/.github/workflows/create-blog-post.yml b/.github/workflows/create-blog-post.yml deleted file mode 100644 index 45cc2d8..0000000 --- a/.github/workflows/create-blog-post.yml +++ /dev/null @@ -1,66 +0,0 @@ -name: Generate Blog Post from Issue -on: - issues: - types: [opened] -jobs: - create-post: - if: contains(github.event.issue.labels.*.name, 'blog') - runs-on: ubuntu-latest - steps: - - name: Checkout Code - uses: actions/checkout@v6 - - - name: Generate GitHub App Token - id: app-token - uses: actions/create-github-app-token@v3 - with: - app-id: ${{ secrets.APP_ID }} - private-key: ${{ secrets.APP_PRIVATE_KEY }} - - - name: Parse Issue Form - id: parse - uses: stefanbuck/github-issue-parser@v3 - with: - template-path: .github/ISSUE_TEMPLATE/01-new-blog-post.yml - - - name: Set up Python - uses: actions/setup-python@v6 - with: - python-version: '3.x' - - - name: Install Dependencies - run: | - python -m pip install --upgrade pip - pip install Pillow python-dateutil - - - name: Process Issue Data and Assets (Python Optimizer) - id: process - env: - ISSUE_JSON: ${{ steps.parse.outputs.jsonString }} - run: | - python .github/scripts/generate_post.py - - - name: Create Pull Request - id: cpr - uses: peter-evans/create-pull-request@v8 - with: - token: ${{ steps.app-token.outputs.token }} - commit-message: "feat(blog): add new post from issue #${{ github.event.issue.number }}" - branch: "automation/issue-${{ github.event.issue.number }}-${{ env.BLOG_FILENAME }}" - title: "feat(blog): ${{ github.event.issue.title }}" - body: | - This Pull Request was automatically generated from Issue #${{ github.event.issue.number }}. - Closes #${{ github.event.issue.number }} - delete-branch: true - labels: | - blog - - name: Comment on Issue with PR Link - if: steps.cpr.outputs.pull-request-number != '' - uses: peter-evans/create-or-update-comment@v5 - with: - token: ${{ steps.app-token.outputs.token }} - issue-number: ${{ github.event.issue.number }} - body: | - 🎉 Success! A staging branch has been created. - - Your blog post is ready for review here:👉 ${{ steps.cpr.outputs.pull-request-url }} \ No newline at end of file diff --git a/.github/workflows/create-doc.yml b/.github/workflows/create-doc.yml deleted file mode 100644 index b1a56bb..0000000 --- a/.github/workflows/create-doc.yml +++ /dev/null @@ -1,69 +0,0 @@ -name: Generate Document from Issue - -on: - issues: - types: [opened] - -jobs: - create-doc: - if: contains(github.event.issue.labels.*.name, 'docs') - runs-on: ubuntu-latest - steps: - - name: Checkout Code - uses: actions/checkout@v6 - - - name: Generate GitHub App Token - id: app-token - uses: actions/create-github-app-token@v3 - with: - app-id: ${{ secrets.APP_ID }} - private-key: ${{ secrets.APP_PRIVATE_KEY }} - - - name: Parse Issue Form - id: parse - uses: stefanbuck/github-issue-parser@v3 - with: - template-path: ./github/ISSUE_TEMPLATE/02-new-document.yml - - - name: Set up Python - uses: actions/setup-python@v6 - with: - python-version: '3.x' - - - name: Install Dependencies - run: | - python -m pip install --upgrade pip - pip install Pillow - - - name: Process Issue Data and Assets (Python Optimizer) - id: process - env: - ISSUE_JSON: ${{ steps.parse.outputs.jsonString }} - run: | - python .github/scripts/generate_docs.py - - - name: Create Pull Request - id: cpr - uses: peter-evans/create-pull-request@v8 - with: - token: ${{ steps.app-token.outputs.token }} - commit-message: "feat(docs): add new doc from issue #${{ github.event.issue.number }}" - branch: "automation/issue-${{ github.event.issue.number }}-${{ env.DOC_FILENAME }}" - title: "feat(docs): ${{ github.event.issue.title }}" - body: | - This Pull Request was automatically generated from Issue #${{ github.event.issue.number }}. - Closes #${{ github.event.issue.number }} - delete-branch: true - labels: | - docs - - - name: Comment on Issue with PR Link - if: steps.cpr.outputs.pull-request-number != '' - uses: peter-evans/create-or-update-comment@v5 - with: - token: ${{ steps.app-token.outputs.token }} - issue-number: ${{ github.event.issue.number }} - body: | - 🎉 Success! A staging branch has been created. - - Your document is ready for review here:👉 ${{ steps.cpr.outputs.pull-request-url }} \ No newline at end of file diff --git a/.github/workflows/process-new-authors.yml b/.github/workflows/process-new-authors.yml deleted file mode 100644 index 839e69d..0000000 --- a/.github/workflows/process-new-authors.yml +++ /dev/null @@ -1,213 +0,0 @@ -# .github/workflows/process-new-authors.yml -name: Process New Author Request -on: - issues: - types: [opened] -jobs: - add-author-and-pr: - if: contains(github.event.issue.labels.*.name, 'new-author') - runs-on: ubuntu-latest - steps: - - name: Checkout Repo - uses: actions/checkout@v6 - - - name: Generate Token - id: app-token - uses: actions/create-github-app-token@v3 - with: - app-id: ${{ secrets.APP_ID }} - private-key: ${{ secrets.APP_PRIVATE_KEY }} - - - name: Parse Issue Form - id: parse - uses: stefanbuck/github-issue-parser@v3 - with: - template-path: .github/ISSUE_TEMPLATE/03-new-blog-author.yml - - - name: Set up Python - uses: actions/setup-python@v6 - with: - python-version: '3.x' - - - name: Install Dependencies - run: | - python -m pip install --upgrade pip - pip install Pillow PyYAML python-dateutil - - - name: Process Author and Update Files - id: process_files - shell: python - env: - ISSUE_DATA: ${{ steps.parse.outputs.jsonString }} - run: | - import json - import yaml - import re - import os - import sys - import urllib.request - from PIL import Image, ImageOps - - AUTHORS_FILE = 'blog/authors.yml' - TEMPLATE_FILE = '.github/ISSUE_TEMPLATE/01-new-blog-post.yml' - AUTHORS_IMG_DIR = 'static/img/blog/authors' - - issue_json = os.environ.get("ISSUE_DATA", "{}") - data = json.loads(issue_json) - author_name = data.get("name", "").strip() - author_title = data.get("title", "").strip() - raw_image_url = data.get("image_url", "").strip() - - image_url = "" - url_match = re.search(r'\((https://[^\)]+)\)', raw_image_url) - if url_match: - image_url = url_match.group(1) - - if not author_name or not author_title: - print("Missing required fields. Exiting.") - sys.exit(1) - - # Read existing content to check for duplicates without rewriting via safe_load - if os.path.exists(AUTHORS_FILE): - with open(AUTHORS_FILE, 'r', encoding='utf-8') as f: - raw_content = f.read() - - # Simple case-insensitive duplicate name check - if f"name: {author_name}" in raw_content or f'name: "{author_name}"' in raw_content: - print(f"::error::The author name '{author_name}' already exists.") - sys.exit(1) - - slug = author_name.lower() - slug = re.sub(r'[^a-z0-9\s-]', '', slug) - slug = re.sub(r'[\s-]+', '-', slug).strip('-') - - final_slug = slug - counter = 1 - - # Scan file text to ensure slug uniqueness - while f"{final_slug}:" in raw_content: - final_slug = f"{slug}-{counter}" - counter += 1 - - final_image_path = "" - if image_url: - os.makedirs(AUTHORS_IMG_DIR, exist_ok=True) - clean_url = image_url.split("?") - _, ext = os.path.splitext(clean_url[0]) - if not ext: - ext = ".jpg" - - tmp_avatar_path = f"/tmp/raw_avatar{ext}" - try: - urllib.request.urlretrieve(image_url, tmp_avatar_path) - - target_file_name = f"{final_slug}.webp" - target_full_path = os.path.join(AUTHORS_IMG_DIR, target_file_name) - - with Image.open(tmp_avatar_path) as img: - img = ImageOps.exif_transpose(img) - if img.mode in ("P", "CMYK"): - img = img.convert("RGBA") - img.thumbnail((500, 500), Image.Resampling.LANCZOS) - img.save(target_full_path, format="WEBP", quality=85) - - final_image_path = f"/img/blog/authors/{target_file_name}" - print(f"Successfully processed and saved avatar to {target_full_path}") - - if os.path.exists(tmp_avatar_path): - os.remove(tmp_avatar_path) - except Exception as e: - print(f"Warning: Failed to download or process avatar image. Error: {e}") - - # Prepare the clean textual representation block manually to preserve docstrings - entry_lines = [ - f"{final_slug}:", - f" name: {author_name}", - f" title: {author_title}", - " page: true" - ] - if final_image_path: - entry_lines.append(f" image_url: {final_image_path}") - - raw_append_block = "\n" + "\n".join(entry_lines) + "\n" - - # Append directly to the bottom of the filesystem document - with open(AUTHORS_FILE, 'a', encoding='utf-8') as f: - f.write(raw_append_block) - print(f"Successfully appended new profile block to {AUTHORS_FILE}") - - # --- 2. REGEX EXTRACT NAMES TO BUILD TEMPLATE DROPDOWN --- - with open(AUTHORS_FILE, 'r', encoding='utf-8') as f: - updated_raw_content = f.read() - - all_names = re.findall(r'^\s*name:\s*["\']?(.*?)["\']?\s*$', updated_raw_content, re.MULTILINE) - all_names = [n.strip() for n in all_names if n.strip()] - all_names.sort() - - yaml_lines = [f" - {name}" for name in all_names] - replacement_string = "\n".join(yaml_lines) - - if os.path.exists(TEMPLATE_FILE): - with open(TEMPLATE_FILE, 'r') as f: - template_content = f.read() - pattern = r'(# AUTHOR_START\n)(.*?)(\n\s*# AUTHOR_END)' - updated_content = re.sub( - pattern, - f"\\1{replacement_string}\\3", - template_content, - flags=re.DOTALL - ) - with open(TEMPLATE_FILE, 'w') as f: - f.write(updated_content) - print(f"Successfully updated dropdown in {TEMPLATE_FILE}") - else: - print(f"Warning: Template file {TEMPLATE_FILE} not found. Skipping dropdown injection.") - - - name: Create Pull Request - if: success() - uses: peter-evans/create-pull-request@v8 - with: - token: ${{ steps.app-token.outputs.token }} - commit-message: "feat: add new blog author via Issue #${{ github.event.issue.number }}" - title: "feat: add new author from Issue #${{ github.event.issue.number }}" - body: | - This PR automatically handles three tasks: - 1. Downloads, optimizes, and names the avatar image matching the unique author slug. - 2. Adds the new author data mapping properties into `blog/authors.yml`. - 3. Re-generates and sorts the author dropdown options inside the issue templates. - Closes #${{ github.event.issue.number }}. - CODEOWNERS have been automatically assigned to review. - branch: "automation/issue-${{ github.event.issue.number }}" - delete-branch: true - labels: | - new-author - - - name: Comment on Success - if: success() - uses: peter-evans/create-or-update-comment@v5 - with: - token: ${{ steps.app-token.outputs.token }} - issue-number: ${{ github.event.issue.number }} - body: | - Hi there! An automated Pull Request has been generated to add you to the blog authors file and update our submission dropdown tools. The project's CODEOWNERS have been notified to review and merge the changes. Once merged, your author profile and dropdown options will be active! - - - name: Handle Duplicate / Failure - if: failure() - uses: actions/github-script@v9 - with: - github-token: ${{ steps.app-token.outputs.token }} - script: | - const issueNumber = context.issue.number; - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issueNumber, - body: "🛑 **Registration Error:** It looks like an author profile with this name already exists in `blog/authors.yml`. Duplicate entries are not allowed. This issue will now be closed automatically." - }); - await github.rest.issues.update({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issueNumber, - state: 'closed', - state_reason: 'not_planned' - }); From c80850f8f3dc11b71bb5ffac331626876af0f98e Mon Sep 17 00:00:00 2001 From: shoverbj Date: Thu, 18 Jun 2026 08:10:57 -0400 Subject: [PATCH 36/50] fixed issue with location of slides folder --- .github/scripts/generate_post.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/scripts/generate_post.py b/.github/scripts/generate_post.py index 0cd5a6d..2448696 100644 --- a/.github/scripts/generate_post.py +++ b/.github/scripts/generate_post.py @@ -280,7 +280,7 @@ def main(): print(f"Failed downloading album image {clean_match_url}: {e}") # Process and optimize the directory full of individual files - optimize_convert_and_hash_images(tmp_slides_dir, static_folder, keep_original_names=False) + optimize_convert_and_hash_images(tmp_slides_dir, slides_static_folder, keep_original_names=False) has_album = True # --- PARSE AND DOWNLOAD IMAGES INSIDE BLOG CONTENT --- From 60f199403de38a98827c8269e0e664866ca119b4 Mon Sep 17 00:00:00 2001 From: shoverbj Date: Thu, 18 Jun 2026 08:47:09 -0400 Subject: [PATCH 37/50] improve llms document --- blog/2026-06-10-scoutmasters-blog.mdx | 15 +- plugins/recent-blog-posts.js | 2 +- static/llms.txt | 374 ++++++++++++++++++++++++++ tmp_llms.md | 61 ----- 4 files changed, 382 insertions(+), 70 deletions(-) delete mode 100644 tmp_llms.md diff --git a/blog/2026-06-10-scoutmasters-blog.mdx b/blog/2026-06-10-scoutmasters-blog.mdx index df2c276..7f1147d 100644 --- a/blog/2026-06-10-scoutmasters-blog.mdx +++ b/blog/2026-06-10-scoutmasters-blog.mdx @@ -10,16 +10,15 @@ came next which included all of the anti-nausea medications to help get through the chemotherapy. Chemo came at 1500 and was not as bad as I had thought. Fifteen minutes for one, then an hour for the second. Today it was Mr. Darland and Mr. McConnell. Kudos to both of them, hospitals tend to make people nervous -because they don't know what to say or do. +because they don't know what to say or do. -{/* truncate */} +{/_ truncate _/} -To be perfectly honest, just your -presence makes all the difference in the world. We laughed and enjoy time -together, and Mrs. McConnell made me some of her famous baked oyster crackers -and seasonings. We refer to this as Scoutmaster Crack, just because of how good -it is and how fast it disappears. +To be perfectly honest, just your presence makes all the difference in the +world. We laughed and enjoyed time together, and Mrs. McConnell made me some of +her famous baked oyster crackers and seasonings. We refer to this as Scoutmaster +Crack, just because of how good it is and how fast it disappears. After chemo, more fluids to keep the Kidneys flushed out and to help move the -chemo along once it has done it's job. Release day is scheduled for Friday, +chemo along once it has done its job. Release day is scheduled for Friday, though given other delays we will wait and see. diff --git a/plugins/recent-blog-posts.js b/plugins/recent-blog-posts.js index 719214e..bd1d003 100644 --- a/plugins/recent-blog-posts.js +++ b/plugins/recent-blog-posts.js @@ -2,7 +2,7 @@ * @file recent-blog-posts.js * @description A custom local Docusaurus plugin decorator that extends the core blog plugin. * Intercepts the build-time data lifecycle hook (`contentLoaded`), filters out unlisted posts, - * truncates the list to the 5 most recent records, and flushes them directly to a local JSON + * truncates the list to the 4 most recent records, and flushes them directly to a local JSON * schema file. This enables client-side components to safely load recent blog metadata without * bundling massive layout trees. * diff --git a/static/llms.txt b/static/llms.txt index bb357de..88bfa6f 100644 --- a/static/llms.txt +++ b/static/llms.txt @@ -13,6 +13,10 @@ - **Source Code:** Hosted at `https://github.com/scouting331/scoutSite`. - **Hosting & CI/CD:** Built automatically by a worker on commit and deployed to `https://brownsburgscouts.org`. +- **Maintenance Philosophy:** Built with extensive automations and thorough + commenting so young youth Scouts with limited coding abilities can easily + maintain the site, while remaining structured cleanly for a seamless future + handoff to experienced adult coders. ## Organization Profile & Unit Context @@ -100,3 +104,373 @@ hide_table_of_contents: true ## CLI Core Commands - **Local Dev:** `npm run start` + +## AI Commenting & Documentation Standards + +All generated code (JavaScript, JSX, Python, Bash) must be heavily commented and +highly descriptive. The codebase serves as a learning tool for young Scouts; +assume the reader has zero prior programming experience. + +### Mandatory Structure for Code Files + +#### 1. JSX / React Components (`.jsx`) + +- **Module Level:** A detailed block comment at the top explaining the + component's purpose, its visual location on the site, and any props it + accepts. +- **Inline Comments:** Explicitly explain _why_ a hook is used (`useState`, + `useEffect`) and break down any logical conditions or array mappings. + +```jsx +/** + * AnnouncementsWidget Component + * + * Purpose: Displays a list of recent unit updates on the landing page. + * Audience: Pack 303 / Troop 303 parents looking for schedules. + * Props: + * - limit (Number): Max number of posts to display. + */ +import React from "react"; + +export default function AnnouncementsWidget({ limit = 3 }) { + // Step 1: Set up a state variable to hold the blog posts we fetch + const [posts, setPosts] = React.useState([]); + + return ( +
+ {/* Loop through each post item and convert it into a visual card */} + {posts.slice(0, limit).map((post) => ( +
+

{post.title}

+
+ ))} +
+ ); +} +``` + +#### 2. Python Scripts (`.py`) + +- **Module Level:** Google-style module docstring outlining the script's global + role in the site's automations. +- **Function Level:** Clear docstrings defining `Args` and `Returns` explicitly. +- **Inline Comments:** Step-by-step logic tracking, avoiding implicit code + shortcuts. + +```python +"""Calendar Sync Automation. + +This script pulls events from the American Legion Post 331 Google Calendar +and formats them into Markdown files for the Docusaurus site schedule. +""" + +def parse_calendar_event(event_data): + """Extracts date, time, and unit details from a raw calendar object. + + Args: + event_data (dict): The raw JSON object returned by the Calendar API. + + Returns: + dict: A cleaned dictionary containing 'title', 'date', and 'target_unit'. + """ + # Initialize an empty dictionary to safely structure our clean data + cleaned_event = {} + + # Extract the summary line (e.g., "Pack 303 Blue & Gold Banquet") + cleaned_event['title'] = event_data.get('summary', 'Untitled Event') + + return cleaned_event +``` + +### Strict LLM Rules for Code Generation + +1. **No "Self-Explanatory" Code:** Never skip comments under the assumption that + the code is clean enough to read without them. +2. **Explain the "Why", Not Just the "What":** Instead of writing + `# sets x to 5`, write + `# Set the fallback limit to 5 so the page doesn't break if the API fails`. +3. **Use Simple Analogies:** When explaining complex coding patterns (like git + filters or regex parsing), include a one-sentence conceptual analogy in the + comment block. + +## Scout & Leader Local Execution Guide + +This section ensures that any Scout or leader can safely run our background +automation scripts (like the Facebook router or calendar syncs) on their +personal computer without breaking the main website. + +### 1. Prerequisites (One-Time Setup) + +Before running any Python scripts, the computer needs Python installed and a +copy of the website code. + +1. **Open the Terminal / Command Prompt:** + - **Windows:** Press the `Windows Key`, type `cmd`, and press Enter. + - **Mac:** Press `Cmd + Space`, type `Terminal`, and press Enter. +2. **Check if Python is installed:** Type `python --version` and press Enter. If + it displays a version number (like `Python 3.11.x`), it is ready. +3. **Navigate to the Project Folder:** Use the change directory (`cd`) command + to move into the downloaded repository folder: + + ```bash + cd path/to/scoutSite + ``` + +### 2. Testing Scripts Safely on Your Computer + +To test scripts locally without affecting the live website or sending accidental +posts to the public Facebook pages, always follow these rules: + +#### Step A: Create a Safe Virtual Environment + +A virtual environment keeps our project's tools separate from the rest of the +computer so nothing gets mixed up. + +```bash +# 1. Create the environment folder (we name it 'scout-env') +python -m venv scout-env + +# 2. Turn it on (Activate it) +# On Windows: +scout-env\Scripts\activate +# On Mac / Linux: +source scout-env/bin/activate +``` + +_Visual cue: Your terminal line will now show `(scout-env)` at the very +beginning._ + +#### Step B: Simulate GitHub Environment Variables + +Our automation scripts look for special environment variables that GitHub +normally provides automatically. When testing on a personal computer, you must +simulate them manually in your terminal before running the script: + +```bash +# Create a dummy mock file for the script to write its outputs to +touch mock_output.txt + +# Tell the computer where that mock file lives +# On Windows (Command Prompt): +set GITHUB_OUTPUT=mock_output.txt +# On Mac / Linux: +export GITHUB_OUTPUT=mock_output.txt +``` + +#### Step C: Run the Script + +Now you can safely trigger the script to see how it behaves: + +```bash +python .github/scripts/fb_router.py +``` + +### 3. Common Error Troubleshooting + +If a Scout runs into an issue, match the terminal error message to the solutions +below: + +#### ❌ Error: `"python" is not recognized as an internal or external command` + +- **What it means:** The computer doesn't know where Python is installed. +- **The Fix:** Download and run the official installer from `python.org`. On + Windows, **make sure to check the box that says "Add python.exe to PATH"** at + the very bottom of the installer window before clicking install. + +#### ❌ Error: `KeyError: 'GITHUB_OUTPUT'` + +- **What it means:** The script tried to save its results to GitHub, but it + couldn't find the target output file pathway. +- **The Fix:** You forgot to run the `set GITHUB_OUTPUT` or + `export GITHUB_OUTPUT` command from Step B. Run that command and try again. + +#### ❌ Error: `Error reading git diff.` / `fatal: not a git repository` + +- **What it means:** The script is trying to look at your recent file changes + using Git history, but you either aren't inside the project folder, or Git + isn't tracking it. +- **The Fix:** Make sure you ran `cd scoutSite` to get into the folder. Type + `git status` to verify that your history tracks properly. + +## Scout & Leader Frontend Local Execution Guide + +This section helps Scouts and leaders launch a local preview of the website on +their computer using Node.js and Node Package Manager (npm). This allows them to +see their markdown edits and component changes live before submitting them to +GitHub. + +### 1. Prerequisites (One-Time Setup) + +To build the user interface, the computer needs Node.js installed to process our +React and Docusaurus files. + +1. **Install Node.js:** Download and run the **LTS (Long Term Support)** + installer from `nodejs.org`. +2. **Verify Installation:** Open your Terminal or Command Prompt and run: + + ```bash + node --version + npm --version + ``` + + _If both commands return numbers (e.g., `v20.x.x` and `10.x.x`), you are + ready to go._ + +3. **Download Project Tools:** Navigate into your `scoutSite` folder and + download the exact construction packages required for our theme: + + ```bash + cd path/to/scoutSite + npm install + ``` + +### 2. Launching the Live Preview Website + +Once your folder setup is complete, you can start the local development server +to test your changes. + +```bash +npm run start +``` + +- **What happens next:** The terminal will compile the pages, turn on a tiny + background server, and automatically open a web browser tab pointing to + `http://localhost:3000`. +- **Live Editing:** Leave this terminal window open! Every time you modify and + save a blog post (`.md`), documentation file, or React component (`.jsx`), the + website in your browser will automatically refresh to show your new changes + within two seconds. +- **How to turn it off:** Go back to your terminal window and press `Ctrl + C` + on your keyboard, then type `Y` if prompted, to turn off the local preview. + +### 3. Common Frontend Error Troubleshooting + +If a Scout hits a wall while trying to run the frontend, look for these common +error signatures: + +#### ❌ Error: `'npm' is not recognized as an internal or external command` + +- **What it means:** The computer doesn't know where Node.js or npm is located. +- **The Fix:** Close your terminal window entirely, reopen it, and try again. If + it still fails, reinstall Node.js from the official site and ensure you don't + uncheck any path settings during installation. + +#### ❌ Error: `Error: Cannot find module '...'` or `sh: docusaurus: command not found` + +- **What it means:** The project folder is missing its core files because the + initial dependency download was skipped or interrupted. +- **The Fix:** Run `npm install` inside the project folder. This creates a fresh + `node_modules/` folder containing all the building blocks Docusaurus needs. + +#### ❌ Error: `Port 3000 is already in use` + +- **What it means:** Another local server is already running on your computer, + or an old Docusaurus session didn't close properly. +- **The Fix:** The terminal will usually ask: _"Would you like to run the server + on another port instead? (Y/n)"_. Simply type `Y` and hit Enter. Docusaurus + will open your site at `http://localhost:3001` instead. + +#### ❌ Error: `Docusaurus found broken links!` + +- **What it means:** A Scout added a link to a page, image, or document that + doesn't actually exist in the repository or has a typo in its path. +- **The Fix:** Read the lines directly below the error message in the terminal. + Docusaurus will tell you exactly which markdown file has the broken link and + what typo it is looking for. Correct the path inside that file and save it. + +## Scout & Leader Git & GitHub Contribution Guide + +This guide ensures that any Scout or volunteer leader can safely pull down code, +make changes, and submit their work for review without accidentally breaking the +live website. + +### 1. The 6-Step Scout Contribution Recipe + +Always use a separate "branch" (a sandbox copy) for your work. Never edit the +`main` branch directly. + +#### Step 1: Get the Latest Code + +Before starting any new work, make sure your computer has the absolute newest +copy of the website from the internet: + +```bash +git checkout main +git pull origin main +``` + +#### Step 2: Create Your Sandbox (Branch) + +Create a new branch named after the task you are doing (use dashes instead of +spaces, and keep it lowercase): + +```bash +# Syntax: git checkout -b scout-name-description +git checkout -b john-d-add-pack303-blog +``` + +#### Step 3: Do Your Work and Test It + +Modify your markdown files, add your photos, or edit your code. Use our +execution guides to test your changes locally: + +- Run `npm run start` to make sure your pages look correct and have no broken + links. +- Run your Python scripts locally to verify your automations. + +#### Step 4: Save Your Progress (Commit) + +Tell Git to take a snapshot of the files you changed. Write a clear, simple +message explaining what you did: + +```bash +# 1. Stage all your changed files to be saved +git add . + +# 2. Save the snapshot with a meaningful description +git commit -m "Add blog post for Pack 303 Blue and Gold Banquet" +``` + +#### Step 5: Upload to GitHub + +Send your branch from your personal computer up to our online repository on +GitHub: + +```bash +git push origin john-d-add-pack303-blog +``` + +#### Step 6: Create a Pull Request (PR) + +1. Go to `https://github.com`. +2. You will see a yellow banner at the top that says **"Compare & pull + request"**. Click it. +3. Write a short note describing your changes so an adult leader or experienced + Scout can review it and merge it into the live site! + +--- + +### 2. Common Git Troubleshooting + +#### ❌ Error: `fatal: Grandma's-Macbook is not a git repository` + +- **What it means:** Your terminal is currently pointing to a generic folder on + your computer rather than our specific website project folder. +- **The Fix:** Move into the correct directory before typing any git commands: + `cd path/to/scoutSite`. + +#### ❌ Error: `error: Your local changes to the following files would be overwritten by checkout...` + +- **What it means:** You modified some files on your computer while working on + the wrong branch, and Git is afraid it will erase your hard work if it + switches tasks. +- **The Fix:** Temporarily save your work in a hidden pocket, switch your + branch, and pull your work back out: + + ```bash + git stash + git checkout main + git pull origin main + git checkout -b your-new-branch-name + git stash pop + ``` diff --git a/tmp_llms.md b/tmp_llms.md deleted file mode 100644 index 9aaa915..0000000 --- a/tmp_llms.md +++ /dev/null @@ -1,61 +0,0 @@ -# American Legion Post 331 Scouting Units - Docusaurus Site Context - -> System architecture and formatting rules for LLMs contributing to this repository. - -## Project Profile -- **Stack:** Docusaurus v3+, React, MDX, JavaScript/TypeScript, Python. -- **Audience:** Scouting America members, parents, and leaders. -- **Design Core:** Custom theme using Scouting America branding (Khaki/Blue/Red). -- **Dark Mode:** Explicitly disabled. Code must only support light mode. - -## Codebase Map & Router Configurations - -### Content Plugins (Multi-Instance Docs) -This site uses multiple instances of the Docusaurus docs plugin. -- `/docs/`: Main instance. Core Scouting unit documents and shared files. Uses `sidebarsDocs.js`. -- `/cookbook/`: Custom instance. Camping recipes ecosystem. Uses `sidebarCookbook.js`. - -### Blogs & Components -- `/blog/`: Unit activity updates. Cross-referenced via `/blog/authors.yml` and `/blog/tags.yml`. -- `/src/pages/`: Main entry point and unit-specific landing pages. -- `/src/components/[ComponentName]/index.jsx`: Modular UI components. Optional localized `styles.module.css` inside the same folder. -- `/static/`: Shared media assets (images, PDFs). Reference using absolute paths (e.g., `/img/logo.png`). -- `/src/css/custom.css`: Target file for global font and CSS variable overrides. - -## Strict Code Conventions - -### Frontmatter Definitions -AI must generate frontmatter matching these strict JSON schemas: - -#### Docs & Cookbook (`/docs/`, `/cookbook/`) -```yaml -title: "Page Title" -description: "SEO description under 160 chars" -``` - -#### Blog Posts (`/blog/`) -```yaml -title: "Post Title" -date: YYYY-MM-DD -authors: [author_key_from_authors_yml] -tags: [unit_tag_from_tags_yml] -``` - -#### MDX Pages (`/src/pages/`) -```yaml -description: "SEO description" -hide_table_of_contents: true -``` - -### Component & Markup Rules -- **Formatting:** All narrative content must use `.mdx` extensions. Custom components must use `.jsx`. -- **Built-ins:** Always default to native ``, ``, and `` components. -- **Custom HTML:** Never write raw inline HTML in markdown. Wrap HTML into a reusable component in `/src/components/`. -- **Modification:** Never modify `node_modules`. Use `npm run swizzle` for layout overrides. - -## CLI Core Commands -- **Local Dev:** `npm run start` -- **Code Storage:** The code is stored and version controlled in a Github - repository at https://github.com/scouting331/scoutSite -- **Site Hosting:** After a pull request, a worker builds the site for - hosting on Cloudflare at https://brownsburgscouts.org From 447099f32e87967551559aa916240a9399283a54 Mon Sep 17 00:00:00 2001 From: shoverbj Date: Thu, 18 Jun 2026 08:48:54 -0400 Subject: [PATCH 38/50] change truncate to be proper --- blog/2026-06-10-scoutmasters-blog.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blog/2026-06-10-scoutmasters-blog.mdx b/blog/2026-06-10-scoutmasters-blog.mdx index 7f1147d..f06554e 100644 --- a/blog/2026-06-10-scoutmasters-blog.mdx +++ b/blog/2026-06-10-scoutmasters-blog.mdx @@ -12,7 +12,7 @@ Fifteen minutes for one, then an hour for the second. Today it was Mr. Darland and Mr. McConnell. Kudos to both of them, hospitals tend to make people nervous because they don't know what to say or do. -{/_ truncate _/} +{/* truncate */} To be perfectly honest, just your presence makes all the difference in the world. We laughed and enjoyed time together, and Mrs. McConnell made me some of From a185816960c688b50ac67afd8e5d2e108ada5fde Mon Sep 17 00:00:00 2001 From: shoverbj Date: Thu, 18 Jun 2026 08:55:57 -0400 Subject: [PATCH 39/50] remove depricated app-id --- .github/workflows/content-management.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/content-management.yml b/.github/workflows/content-management.yml index 0e9329e..9e6d6af 100644 --- a/.github/workflows/content-management.yml +++ b/.github/workflows/content-management.yml @@ -17,7 +17,7 @@ jobs: id: app-token uses: actions/create-github-app-token@v3 with: - app-id: ${{ secrets.APP_ID }} + client-id: ${{ secrets.APP_ID }} private-key: ${{ secrets.APP_PRIVATE_KEY }} - name: Parse Issue Form @@ -81,7 +81,7 @@ jobs: id: app-token uses: actions/create-github-app-token@v3 with: - app-id: ${{ secrets.APP_ID }} + client-id: ${{ secrets.APP_ID }} private-key: ${{ secrets.APP_PRIVATE_KEY }} - name: Parse Issue Form @@ -145,7 +145,7 @@ jobs: id: app-token uses: actions/create-github-app-token@v3 with: - app-id: ${{ secrets.APP_ID }} + client-id: ${{ secrets.APP_ID }} private-key: ${{ secrets.APP_PRIVATE_KEY }} - name: Parse Issue Form From a819c70e3c158aad94ba54f9cee39cfda7295ef3 Mon Sep 17 00:00:00 2001 From: shoverbj Date: Thu, 18 Jun 2026 09:01:46 -0400 Subject: [PATCH 40/50] add words to dictionary --- .github/.scout-dictionary | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/.scout-dictionary b/.github/.scout-dictionary index df7f242..0a6f8ae 100644 --- a/.github/.scout-dictionary +++ b/.github/.scout-dictionary @@ -3,13 +3,20 @@ brookville Campmor clifty Crocs +dateutil +env epi +EXIF fiorella firem'n +idx +img iols jaccos +kebabed koczan krietenstein +LANCZOS leppert lubbe maumee @@ -17,7 +24,9 @@ mmr neese oa ons +PIL sharin +slugified stoddard totin' towne From 3da1475bf2ec4b700a5175fc43cb395b7e7d6add Mon Sep 17 00:00:00 2001 From: shoverbj Date: Thu, 18 Jun 2026 09:33:10 -0400 Subject: [PATCH 41/50] improvements --- .github/ISSUE_TEMPLATE/01-new-blog-post.yml | 18 ++++++++++++++++-- .github/scripts/generate_post.py | 2 ++ .vscode/extensions.json | 12 ++++++++++++ blog/2026-04-27-western-camporee.mdx | 3 ++- blog/2026-04-28-railroading.mdx | 3 ++- blog/2026-06-17-grant-b.mdx | 1 + 6 files changed, 35 insertions(+), 4 deletions(-) create mode 100644 .vscode/extensions.json diff --git a/.github/ISSUE_TEMPLATE/01-new-blog-post.yml b/.github/ISSUE_TEMPLATE/01-new-blog-post.yml index 8d8c133..3694cd7 100644 --- a/.github/ISSUE_TEMPLATE/01-new-blog-post.yml +++ b/.github/ISSUE_TEMPLATE/01-new-blog-post.yml @@ -22,7 +22,7 @@ body: This should include the campout/adventure/event if thats what this post is about! - placeholder: "Our Recent Adventures" + placeholder: "👉 REPLACE WITH YOUR TITLE 👈" validations: required: true - type: input @@ -104,7 +104,21 @@ body: label: "Blog Text" description: | Write your blog post here! - placeholder: "Blog text here..." + placeholder: | + + + ## What We Did + + - Activity 1 + - Activity 2 + - Activity 3 + + :::info⛺ Outdoor Adventure Tip + + If this post is about a camping trip, use this box to share a tip about + the gear or recipes we used! + + ::: validations: required: true - type: markdown diff --git a/.github/scripts/generate_post.py b/.github/scripts/generate_post.py index 2448696..adc5380 100644 --- a/.github/scripts/generate_post.py +++ b/.github/scripts/generate_post.py @@ -393,6 +393,8 @@ def main(): mdx_file.write(f"authors: [{front_matter_authors}]\n") if tags: mdx_file.write(f"tags: [{front_matter_tags}]\n") + if cover_line: + mdx_file.write(f"image: {web_prefix}/cover.webp") mdx_file.write(f"---\n\n") if import_line: mdx_file.write(import_line) diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..c31655f --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,12 @@ +// .vscode/extensions.json +{ + "recommendations": [ + "esbenp.prettier-vscode", + "streetsidesoftware.code-spell-checker", + "DavidAnson.vscode-markdownlint", + "ms-python.python", + "redhat.vscode-yaml", + "github.vscode-github-actions", + "GitHub.vscode-pull-request-github" + ] +} \ No newline at end of file diff --git a/blog/2026-04-27-western-camporee.mdx b/blog/2026-04-27-western-camporee.mdx index 6b29d27..66c4c3d 100644 --- a/blog/2026-04-27-western-camporee.mdx +++ b/blog/2026-04-27-western-camporee.mdx @@ -3,11 +3,12 @@ title: Western Division Spring Camporee date: 2026-04-27 authors: [benjamin-shover] tags: [troop-303] +image: /img/blog/2026-04-27-western-camporee/cover.webp --- import PhotoAlbumGallery from '@site/src/components/PhotoAlbumGallery'; -![Departure Picture](/img/blog/2026-04-27-western-camporee/cover.webp) +![Cover Photo](/img/blog/2026-04-27-western-camporee/cover.webp) The Scouts of the Legendary Troop 303 attended the 2026 Western Division Spring Camporee this past weekend. diff --git a/blog/2026-04-28-railroading.mdx b/blog/2026-04-28-railroading.mdx index 396af9d..b7b63a3 100644 --- a/blog/2026-04-28-railroading.mdx +++ b/blog/2026-04-28-railroading.mdx @@ -3,9 +3,10 @@ title: Railroading Camporee date: 2026-04-28 authors: [benjamin-shover] tags: [troop-331] +image: /img/blog/2026-04-28-railroading/cover.webp --- -![Blog Cover Photo](/img/blog/2026-04-28-railroading/cover.webp) +![Cover Photo](/img/blog/2026-04-28-railroading/cover.webp) The Scouts of Troop 331 went out of council to attend the Hoosier Trails Council's Railroading Camporee. diff --git a/blog/2026-06-17-grant-b.mdx b/blog/2026-06-17-grant-b.mdx index e59b47e..f4f6ea3 100644 --- a/blog/2026-06-17-grant-b.mdx +++ b/blog/2026-06-17-grant-b.mdx @@ -3,6 +3,7 @@ title: "Congratulations to Grant B" date: 2026-06-17 authors: [benjamin-shover] tags: [troop-303] +image: /img/blog/2026-06-17-grant-b/cover.webp --- import PhotoAlbumGallery from '@site/src/components/PhotoAlbumGallery'; From 215c1b5573b8eeae9aaaedd8ce1615d55741da78 Mon Sep 17 00:00:00 2001 From: shoverbj Date: Thu, 18 Jun 2026 10:33:40 -0400 Subject: [PATCH 42/50] seo changes --- .github/.scout-dictionary | 10 +-- .github/scripts/generate_post.py | 49 ++++++++++++- docusaurus.config.js | 119 ++++++++++++++++++------------- plugins/recent-blog-posts.js | 15 +++- src/pages/crew-303/index.mdx | 2 +- src/pages/pack-303/index.mdx | 2 +- src/pages/troop-303/index.mdx | 2 +- src/pages/troop-331/index.mdx | 2 +- wrangler.jsonc | 33 ++++++++- 9 files changed, 171 insertions(+), 63 deletions(-) diff --git a/.github/.scout-dictionary b/.github/.scout-dictionary index 0a6f8ae..818cf35 100644 --- a/.github/.scout-dictionary +++ b/.github/.scout-dictionary @@ -1,12 +1,12 @@ boonie brookville -Campmor +campmor clifty -Crocs +crocs dateutil env epi -EXIF +exif fiorella firem'n idx @@ -16,7 +16,7 @@ jaccos kebabed koczan krietenstein -LANCZOS +lanczos leppert lubbe maumee @@ -24,7 +24,7 @@ mmr neese oa ons -PIL +pil sharin slugified stoddard diff --git a/.github/scripts/generate_post.py b/.github/scripts/generate_post.py index adc5380..20adaea 100644 --- a/.github/scripts/generate_post.py +++ b/.github/scripts/generate_post.py @@ -38,6 +38,48 @@ from PIL import Image, ImageOps # --- Helper functions --- +def generate_clean_description(blog_content): + """Parses raw markdown content to extract a clean 150-character SEO description. + + Removes code blocks, images, headings, and markdown syntax markup to form + a punchy, reader-friendly string that fits within Google snippet limits. + + Args: + blog_content (str): The raw text string from the 'blog-content' form field. + + Returns: + str: A clean text summary exactly 150 characters or less with an ellipsis. + """ + if not blog_content or len(blog_content.strip()) < 15: + # High-utility localized fallback text if the body field was left empty + return "Discover recent outdoor adventures, volunteer service projects, and youth leadership milestones from our Brownsburg, IN Scouting units." + + # 1. Clean out the heaviest non-text elements completely + text = re.sub(r'```.*?```', '', blog_content, flags=re.DOTALL) # Remove code blocks + text = re.sub(r'!\[.*?\]\(.*?\)', '', text) # Remove markdown images + text = re.sub(r'', '', text) # Remove HTML images + text = re.sub(r'<.*?>.*?', '', text, flags=re.DOTALL) # Remove HTML tags/components + text = re.sub(r'#+\s+.*', '', text) # Remove headers (# Title) + + # 2. Strip out inline styling symbols + text = re.sub(r'[*_`~]', '', text) # Strip bold, italics, code strings, strikethroughs + text = re.sub(r'\[(.*?)\]\(.*?\)', r'\1', text) # Convert [Click Here](url) -> Click Here + text = re.sub(r'^[\-\*\+]\s+', '', text, flags=re.MULTILINE) # Strip out list item dashes/bullets + + # 3. Flatten lines breaks, tabs, and duplicate white space + text = " ".join(text.split()) + + # 4. Enforce strict 145-character boundary rule to protect against truncation + if len(text) > 145: + # Clip back to the last complete word so words aren't cut in half + text = text[:145].rsplit(' ', 1)[0].strip() + + # Ensure the sentence has an appropriate grammatical ending + if not text.endswith(('.', '!', '?')): + text += "..." + + return text + def optimize_convert_and_hash_images(input_dir, output_dir, max_size=(1920, 1080), quality=80, keep_original_names=False): """ Optimizes, resizes, and converts images in an input directory to WebP format, @@ -312,6 +354,10 @@ def main(): # Swap original markdown URLs with the newly generated WebP image routes for orig_url, webp_filename in inline_map.items(): blog_content = blog_content.replace(orig_url, f"{web_prefix}/{webp_filename}") + + # --- NEW: GENERATE THE AUTOMATED SEO DESCRIPTION --- + # Extracts text out of the final modified content string right before writing the file + seo_description = generate_clean_description(blog_content) # --- WRITE OUT THE MDX POST FILE --- os.makedirs("blog", exist_ok=True) @@ -388,13 +434,14 @@ def main(): with open(mdx_filepath, "w", encoding="utf-8") as mdx_file: mdx_file.write(f"---\n") mdx_file.write(f"title: \"{safe_title}\"\n") + mdx_file.write(f"description: \"{seo_description}\"\n") mdx_file.write(f"date: {date_str}\n") if authors: mdx_file.write(f"authors: [{front_matter_authors}]\n") if tags: mdx_file.write(f"tags: [{front_matter_tags}]\n") if cover_line: - mdx_file.write(f"image: {web_prefix}/cover.webp") + mdx_file.write(f"image: {web_prefix}/cover.webp\n") mdx_file.write(f"---\n\n") if import_line: mdx_file.write(import_line) diff --git a/docusaurus.config.js b/docusaurus.config.js index 4b8dc00..14f7292 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -14,62 +14,66 @@ import { themes as prismThemes } from "prism-react-renderer"; * Global configuration data schema structure for Docusaurus system operations. * * @type {import('@docusaurus/types').Config} - * @property {string} title - Primary core website branding text headline. - * @property {string} tagline - SEO and card fallback metadata summary description block. - * @property {Object} future - Flag registry optimizing compatibility properties with modern up-stream tools. - * @property {Object} customFields - Storage object injecting custom corporate legal copyright labels into the runtime environment. - * @property {Array>} presets - Classic Docusaurus preset bundle setups handling theme layouts and core document paths. - * @property {Array>} plugins - Custom multi-instance document routes and isolated post processors parsing recent content folders. - * @property {import('@docusaurus/types').ThemeConfig} themeConfig - The master styling architecture setting default light-modes, banners, nav bars, and HTML social links. */ +// 🔔 ANNOUNCEMENT BANNER TOGGLE: Set this to true to turn on an alert bar at the top of every page. const SHOW_ANNOUNCEMENT = false; const config = { + // --- CORE WEBSITE IDENTITY --- title: "The Scouting Units of American Legion Post 331", tagline: - "Scouting America Units Troop 303, Troop 331, Crew 303, and Pack 303 of Brownsburg, Indiana", + "Discover character, leadership, and outdoor adventure for youth ages 5-20 with the Brownsburg, IN Scouting America units at Post 331.", favicon: "img/logos/favicon.png", - // Future flags, see https://docusaurus.io/docs/api/docusaurus-config#future + // Future flags ensure our code remains compatible with upcoming major versions of Docusaurus. + // See https://docusaurus.io#future future: { v4: true, // Improve compatibility with the upcoming Docusaurus v4 }, - // Set the production url of your site here + // The live public web address where parents and the community access the site. url: "https://brownsburgscouts.org", - baseUrl: "/", + baseUrl: "/", // Tells the server that the website is installed at the root directory level - onBrokenLinks: "throw", - onBrokenAnchors: "ignore", + // Guardrails to prevent broken links from going live. + onBrokenLinks: "throw", // ❌ STOPS the build process immediately if a Scout links to a page or image that does not exist. + onBrokenAnchors: "ignore", // 🟡 Ignores minor section-header anchor tag mistakes so they don't break the build pipeline. + // Internationalization settings (Language control). i18n: { defaultLocale: "en", locales: ["en"], }, - trailingSlash: false, + trailingSlash: false, // Ensures consistent URL structures across the site for better search visibility. + // Reusable custom text values that can be dropped into page footers or layouts dynamically. customFields: { copyright1: `© ${new Date().getFullYear()} The Scouting Units of American Legion Post 331, Scouting America`, copyright2: `All Rights Reserved`, }, + // --- CORE WEBSITE PRESETS --- presets: [ [ "classic", { + // We set core blog/docs to false here because we manage them manually below using multi-instance settings. blog: false, docs: false, theme: { - customCss: "./src/css/custom.css", + customCss: "./src/css/custom.css", // The central styling file for changing fonts and brand colors. }, - sitemap: { + // --- SEARCH ENGINE SITEMAP ENGINE --- + // Dynamically turns off sitemap generation during Cloudflare Preview builds to save build time for the Scouts. + sitemap: process.env.SKIP_SITEMAP == "true" ? false : { lastmod: "date", changefreq: "weekly", priority: 0.5, - ignorePatterns: ["/tags/**"], + ignorePatterns: ["/tags/**"], // Bypasses internal organization tags to keep search results clean. filename: "sitemap.xml", + // Custom filter that strips out pagination pages (like /page/2) from search engine results. createSitemapItems: async (params) => { const { defaultCreateSitemapItems, ...rest } = params; const items = await defaultCreateSitemapItems(rest); @@ -80,25 +84,29 @@ const config = { ], ], + // --- CUSTOM WEBSITE PLUGINS & EXTENSIONS --- plugins: [ + // 📖 MULTI-INSTANCE DOCS #1: The Camping Cookbook section. [ "@docusaurus/plugin-content-docs", { id: "cookbook", - path: "cookbook", - routeBasePath: "cookbook", - sidebarPath: "./sidebarCookbook.js", + path: "cookbook", // Looks for folder named 'cookbook' in the root of the project. + routeBasePath: "cookbook", // Makes the website URL point to brownsburgscouts.org/cookbook. + sidebarPath: "./sidebarCookbook.js", // The control panel file managing the cookbook's left-hand menu tree. }, ], + // 📖 MULTI-INSTANCE DOCS #2: Core unit documents and shared files. [ "@docusaurus/plugin-content-docs", { id: "docs", - path: "docs", - routeBasePath: "docs", - sidebarPath: "./sidebarDocs.js", + path: "docs", // Looks for folder named 'docs' in the root of the project. + routeBasePath: "docs", // Makes the website URL point to brownsburgscouts.org/docs. + sidebarPath: "./sidebarDocs.js", // The control panel file managing the core documentation menu tree. }, ], + // ✍️ CUSTOM EXTENSION: Automated recent adventure post processor. [ "./plugins/recent-blog-posts", { @@ -109,6 +117,7 @@ const config = { onInlineTags: "warn", onInlineAuthors: "warn", onUntruncatedBlogPosts: "warn", + // 💡 SAFETY OVERRIDE: Automatically assigns a default Scouting logo if a Scout forgets to add an author profile photo. processBlogPosts: async ({ blogPosts }) => { const DEFAULT_IMAGE = '/img/logos/favicon.png'; @@ -116,8 +125,7 @@ const config = { if (post.metadata && post.metadata.authors) { post.metadata.authors = post.metadata.authors.map((author) => ({ ...author, - // If imageURL is missing, use the default image - imageURL: author.imageURL || DEFAULT_IMAGE, + imageURL: author.imageURL || DEFAULT_IMAGE, // Fallback safety catch })); } return post; @@ -125,12 +133,9 @@ const config = { }, }, ], + // 🍪 PRIVACY COMPLIANCE: Optional cookie consent pop-up banner. + // Set 'enabled: true' if you decide to activate tracking analytics in the future. [ - // Capability for Cookie Consent pop-up if determined that it is necessary - // for our site. - // See https://github.com/mcclowes/docusaurus-plugin-cookie-consent for - // more configuration details - // Privacy and Cookie policy pages would need to be created. 'docusaurus-plugin-cookie-consent', { title: 'Cookie Consent', @@ -148,29 +153,31 @@ const config = { ], ], + // --- VISUAL THEME ARCHITECTURE & UI LAYOUTS --- themeConfig: { - image: "img/logos/favicon.png", + image: "img/logos/favicon.png", // Default image used when links are shared on text messages or social cards. colorMode: { respectPrefersColorScheme: true, - disableSwitch: true, + disableSwitch: true, // Forces light mode across the site to guarantee crisp visibility of unit layouts. defaultMode: "light", }, - // Capability to add an announcement on the top of all pages - // Simply add text to the 'content' and change the "SHOW_ANNOUNCEMENT" - // variable above to true. + // Configures the header banner alert when active. Controlled by the SHOW_ANNOUNCEMENT toggle at the top of this file. announcementBar: SHOW_ANNOUNCEMENT ? { id: "announcement-bar", content: 'This is an announcement', - backgroundColor: "var(--announcement-bar)", + backgroundColor: "var(--announcement-bar)", // Links to a color variable set in src/css/custom.css textColor: "var(--scouting-america-white)", isCloseable: true, } : undefined, + + // --- NAVIGATION BAR CONFIGURATION --- navbar: { title: "Scouting America", logo: { alt: "Scouting America Units", src: "img/logos/all-units-logo.png", }, + // Left and right aligned items sitting at the top of the webpage. items: [ { type: "dropdown", @@ -192,13 +199,15 @@ const config = { to: "/join-us", label: "Join Us", position: "right", - className: "button button--secondary", + className: "button button--secondary", // Applies a standalone decorative theme button styling. }, ], - hideOnScroll: false, + hideOnScroll: false, // Keeps navigation links immediately accessible at the top while reading down pages. }, + + // --- FOOTER SECTION --- footer: { - style: "dark", + style: "dark", // Employs the charcoal/black theme layout block at the bottom of the page. links: [ { title: "Quick Links", @@ -277,6 +286,7 @@ const config = { }, ], }, + // --- CONTACT US FOOTER COLUMN --- { title: "Contact Us", items: [ @@ -294,6 +304,7 @@ const config = { }, { html: ` + {/* Clicking this automatically opens a pre-addressed email window on the user's phone or computer */}
@@ -306,31 +317,41 @@ const config = { }, ], }, + // --- CODE BLOCKS SYNTAX HIGHLIGHTING --- + // Controls how programming snippets look when displayed in documentation tutorials or cookbook instructions. prism: { - theme: prismThemes.github, - darkTheme: prismThemes.dracula, + theme: prismThemes.github, // Uses clean light colors matching general GitHub documentation layouts. + darkTheme: prismThemes.dracula, // Fallback dark color block theme format. }, + // --- MERMAID DIAGRAM OPERATOR --- + // Configures flowchart layout trees so we can build unit organization maps using text commands. mermaid: { options: { - securityLevel: "loose", + securityLevel: "loose", // Necessary to allow custom CSS styling tags to color our flow charts properly. }, }, + // --- GLOBAL SEO GOOGLE KEYWORDS --- + // These hidden search tokens help parents in Brownsburg, Indiana find our Scouting units when searching on Google. metadata: [ { name: "keywords", content: - "eagle scout, webelos, scouts bsa, boy scouts near me, sea scouts, scoutbook, Venture, bsa, Boy Scouts of America, cub scouts, scouts, kids events near me, kid friendly activities near me, fun places for kids near me, scout, boy scouts, Scouting America, Things to do with kids near me, Kids activities near me, kids activities, child development, kids fun near me, trails near me, crafts for kids, Tent camping near me, science experiments for kids, science projects for kids, stem for kids, Canoe, trails near me, hiking trails near me, all trails, campsites, walking trails near me, Camping, Campground, Hiking near me, Camping near me, campgrounds near me, hiking trails near me, Fishing, Swimming, Brownsburg scout troops, Brownsburg kids, find cub scouts near me, find boy scouts near me, find girl scouts near me", + "scouts bsa brownsburg, cub scouts near me, brownsburg scout troops, troop 303 brownsburg, troop 331 indiana, pack 303 indiana, crew 303 ventilation, scouting america indiana, boy scouts brownsburg indiana, girl scouts bsa hendricks county, youth groups brownsburg in, kids activities brownsburg indiana, kid friendly clubs near me, youth leadership programs, eagle scout rank, cub scout advancement, kids outdoor activities hendricks county, family camping brownsburg, stem activities for kids indiana, youth community service brownsburg, child development groups, scouts bsa girls troop, cub scouts avon indiana, boy scouts pittsboro in, youth sports and adventure brownsburg, child character building programs, community youth organizations indiana", }, ], }, - themes: ["@docusaurus/theme-mermaid"], + + // --- MARKDOWN & PARSING ENGINES --- + themes: ["@docusaurus/theme-mermaid"], // Extends theme engine capabilities to natively render Mermaid charts. markdown: { - format: "mdx", - mermaid: true, - emoji: true, + format: "mdx", // Enforces rich MDX format so we can embed custom interactive buttons inside text files. + mermaid: true, // Turns on graph generation tools within standard markdown documents. + emoji: true, // Allows Scouts to write basic shortcuts like :tent: or :fire: to automatically show visual emojis. + + // --- SAFETY HOOKS & COMPILATION GUARDRAILS --- hooks: { - onBrokenMarkdownLinks: "warn", - onBrokenMarkdownImages: "throw", + onBrokenMarkdownLinks: "warn", // 🟡 Warns us in the terminal if a text link points to an invalid section header anchor. + onBrokenMarkdownImages: "throw", // ❌ CRASHES the local builder instantly if a Scout tries to link a photo that is missing. }, }, }; diff --git a/plugins/recent-blog-posts.js b/plugins/recent-blog-posts.js index bd1d003..4b97d87 100644 --- a/plugins/recent-blog-posts.js +++ b/plugins/recent-blog-posts.js @@ -26,6 +26,7 @@ const defaultBlogPlugin = blogPluginExports.default; */ async function blogPluginEnhanced(...pluginArgs) { const blogPluginInstance = await defaultBlogPlugin(...pluginArgs); + // This is the hidden background folder where Docusaurus builds temporary files const dir = ".docusaurus"; return { @@ -43,25 +44,33 @@ async function blogPluginEnhanced(...pluginArgs) { * @returns {Promise} Resolves when downstream base core operations complete execution. */ contentLoaded: async function (data) { + // Step 1: Create a safe copy of all existing blog posts let recentPosts = [...data.content.blogPosts] - // Only show published posts. + // Step 2: Remove any posts marked as hidden or unlisted .filter((p) => !p.metadata.unlisted) + // Step 3: Cut the list down to only keep the 4 most recent adventures .slice(0, 4); + // Step 4: Clean up the data layout to keep the file size incredibly tiny recentPosts = recentPosts.map((p) => { return { id: p.id, metadata: { + // Safely import title, date, permalink, description, tags, and processed author arrays ...p.metadata, }, }; }); + // Step 5: Make sure the hidden tracking folder exists so the computer doesn't crash fs.mkdirSync(dir, { - recursive: true, // Avoid error if directory already exists. + recursive: true, // If the folder already exists, safely skip creating a new one }); - fs.writeFileSync(`${dir}/recent-posts.json`, JSON.stringify(recentPosts)); + // Step 6: Convert the post list into a plain-text file so front-end widgets can load it quickly + fs.writeFileSync(`${dir}/recent-posts.json`, JSON.stringify(recentPosts, null, 2)); + + // Step 7: Tell Docusaurus to finish setting up the rest of the website normally return blogPluginInstance.contentLoaded(data); }, }; diff --git a/src/pages/crew-303/index.mdx b/src/pages/crew-303/index.mdx index d440595..7b2e0a8 100644 --- a/src/pages/crew-303/index.mdx +++ b/src/pages/crew-303/index.mdx @@ -1,5 +1,5 @@ --- -description: The internet home of Crew 303 +description: "Ready for high adventure? Crew 303 is a co-ed outdoor club for older youth (Ages 14-20) in Brownsburg, IN focusing on advanced camping and peer independence." hide_table_of_contents: true --- diff --git a/src/pages/pack-303/index.mdx b/src/pages/pack-303/index.mdx index f55fd02..e434776 100644 --- a/src/pages/pack-303/index.mdx +++ b/src/pages/pack-303/index.mdx @@ -1,5 +1,5 @@ --- -description: The internet home of Pack 303 +description: "Fun, family-centered camping and life skills for elementary youth (Grades K-5) in Brownsburg, IN. Build a lifetime foundation of leadership with Pack 303!" hide_table_of_contents: true --- diff --git a/src/pages/troop-303/index.mdx b/src/pages/troop-303/index.mdx index 65553b7..3b2678e 100644 --- a/src/pages/troop-303/index.mdx +++ b/src/pages/troop-303/index.mdx @@ -1,5 +1,5 @@ --- -description: The internet home of The Legendary Troop 303 +description: "Join the legendary Troop 303 in Brownsburg, IN! We build leadership and character through high-adventure outdoor camping for young men ages 11-17." hide_table_of_contents: true --- diff --git a/src/pages/troop-331/index.mdx b/src/pages/troop-331/index.mdx index 5f01566..0922dcf 100644 --- a/src/pages/troop-331/index.mdx +++ b/src/pages/troop-331/index.mdx @@ -1,5 +1,5 @@ --- -description: The internet home of Troop 331 +description: "Empowering tomorrow's female leaders. Troop 331 in Brownsburg, IN offers a youth-led outdoor adventure program helping young women achieve the Eagle Scout rank." hide_table_of_contents: true --- diff --git a/wrangler.jsonc b/wrangler.jsonc index bf27353..4c80258 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -1,14 +1,45 @@ { + // Links the file to an auto-validation schema so your code editor can catch typos "$schema": "node_modules/wrangler/config-schema.json", + + // The official unique identifying name for this Cloudflare Pages project deployment "name": "scouting331site", + + // Locked date ensuring Cloudflare preserves exact software feature behaviors from this day onward "compatibility_date": "2026-05-21", + + // Enables real-time server tracking logs to trace system performance and catch runtime errors "observability": { "enabled": true }, + + // Informs the Cloudflare engine where your compiled production website files live "assets": { "directory": "build" }, + + // Compatibility switches allowing our frontend server engine to run native Node.js commands "compatibility_flags": [ "nodejs_compat" - ] + ], + + // =========================================================================== + // ENVIRONMENT OVERRIDES FOR SITES AND PREVIEWS + // =========================================================================== + "env": { + // 💡 IMPORTANT FOR SCOUTS: These rules run ONLY on pull requests and branch preview drafts. + // They modify our development pipeline so that testing builds compile much faster. + "preview": { + "vars": { + // Runs Docusaurus in raw developer compilation mode instead of heavy optimization mode + "NODE_ENV": "development", + + // Stops heavy Javascript compression and squeezing to save valuable build minutes + "DISABLE_MINIFICATION": "true", + + // Tells Docusaurus to skip making giant search engine maps for temporary test urls + "SKIP_SITEMAP": "true" + } + } + } } From f94827c997402302e4b3c5b5cb41c546afb755b8 Mon Sep 17 00:00:00 2001 From: shoverbj Date: Thu, 18 Jun 2026 10:44:55 -0400 Subject: [PATCH 43/50] update cloudflare node version --- wrangler.jsonc | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/wrangler.jsonc b/wrangler.jsonc index 4c80258..dc6e1b7 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -23,6 +23,14 @@ "nodejs_compat" ], + // =========================================================================== + // GLOBAL PRODUCTION ENVIRONMENT SETTINGS + // =========================================================================== + "vars": { + // 🚀 FORCES THE ACCELERATED LIVE MAIN WEBSITE TO COMPILE ON NODE 24 + "NODE_VERSION": "24" + }, + // =========================================================================== // ENVIRONMENT OVERRIDES FOR SITES AND PREVIEWS // =========================================================================== @@ -31,6 +39,9 @@ // They modify our development pipeline so that testing builds compile much faster. "preview": { "vars": { + // 🚀 FORCES SCOUT DRAFT PREVIEW BUILD LINKS TO COMPILE ON NODE 24 TOO + "NODE_VERSION": "24", + // Runs Docusaurus in raw developer compilation mode instead of heavy optimization mode "NODE_ENV": "development", From e6e901a99f0490866b620403f0385f55bf76805d Mon Sep 17 00:00:00 2001 From: shoverbj Date: Thu, 18 Jun 2026 10:57:44 -0400 Subject: [PATCH 44/50] clean up of config file --- docusaurus.config.js | 114 +++++++++++++++++++++++-------------------- 1 file changed, 60 insertions(+), 54 deletions(-) diff --git a/docusaurus.config.js b/docusaurus.config.js index 14f7292..86694c0 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -1,9 +1,9 @@ /** * @file docusaurus.config.js * @description Master Node.js configuration schema engine for the American Legion Post 331 Scouting website. - * Declares localized metadata paths, custom theme configurations, multi-instance plugin documentation paths + * Declares localized metadata paths, custom theme configurations, multi-instance plugin documentation paths * (Standard Docs + Cookbooks), custom localized sitemap filtration schemes, and third-party tracking scripts. - * + * * @environment Node.js (Build-time compilation script) * @see {@link https://docusaurus.io/docs/api/docusaurus-config | Docusaurus Configuration API Documentation} */ @@ -12,7 +12,7 @@ import { themes as prismThemes } from "prism-react-renderer"; /** * Global configuration data schema structure for Docusaurus system operations. - * + * * @type {import('@docusaurus/types').Config} */ @@ -37,8 +37,8 @@ const config = { baseUrl: "/", // Tells the server that the website is installed at the root directory level // Guardrails to prevent broken links from going live. - onBrokenLinks: "throw", // ❌ STOPS the build process immediately if a Scout links to a page or image that does not exist. - onBrokenAnchors: "ignore", // 🟡 Ignores minor section-header anchor tag mistakes so they don't break the build pipeline. + onBrokenLinks: "throw", // ❌ STOPS the build process immediately if a Scout links to a page or image that does not exist. + onBrokenAnchors: "ignore", // 🟡 Ignores minor section-header anchor tag mistakes so they don't break the build pipeline. // Internationalization settings (Language control). i18n: { @@ -67,19 +67,22 @@ const config = { }, // --- SEARCH ENGINE SITEMAP ENGINE --- // Dynamically turns off sitemap generation during Cloudflare Preview builds to save build time for the Scouts. - sitemap: process.env.SKIP_SITEMAP == "true" ? false : { - lastmod: "date", - changefreq: "weekly", - priority: 0.5, - ignorePatterns: ["/tags/**"], // Bypasses internal organization tags to keep search results clean. - filename: "sitemap.xml", - // Custom filter that strips out pagination pages (like /page/2) from search engine results. - createSitemapItems: async (params) => { - const { defaultCreateSitemapItems, ...rest } = params; - const items = await defaultCreateSitemapItems(rest); - return items.filter((item) => !item.url.includes("/page/")); - }, - }, + sitemap: + process.env.SKIP_SITEMAP == "true" + ? false + : { + lastmod: "date", + changefreq: "weekly", + priority: 0.5, + ignorePatterns: ["/tags/**"], // Bypasses internal organization tags to keep search results clean. + filename: "sitemap.xml", + // Custom filter that strips out pagination pages (like /page/2) from search engine results. + createSitemapItems: async (params) => { + const { defaultCreateSitemapItems, ...rest } = params; + const items = await defaultCreateSitemapItems(rest); + return items.filter((item) => !item.url.includes("/page/")); + }, + }, }, ], ], @@ -91,8 +94,8 @@ const config = { "@docusaurus/plugin-content-docs", { id: "cookbook", - path: "cookbook", // Looks for folder named 'cookbook' in the root of the project. - routeBasePath: "cookbook", // Makes the website URL point to brownsburgscouts.org/cookbook. + path: "cookbook", // Looks for folder named 'cookbook' in the root of the project. + routeBasePath: "cookbook", // Makes the website URL point to brownsburgscouts.org/cookbook. sidebarPath: "./sidebarCookbook.js", // The control panel file managing the cookbook's left-hand menu tree. }, ], @@ -101,8 +104,8 @@ const config = { "@docusaurus/plugin-content-docs", { id: "docs", - path: "docs", // Looks for folder named 'docs' in the root of the project. - routeBasePath: "docs", // Makes the website URL point to brownsburgscouts.org/docs. + path: "docs", // Looks for folder named 'docs' in the root of the project. + routeBasePath: "docs", // Makes the website URL point to brownsburgscouts.org/docs. sidebarPath: "./sidebarDocs.js", // The control panel file managing the core documentation menu tree. }, ], @@ -119,7 +122,7 @@ const config = { onUntruncatedBlogPosts: "warn", // 💡 SAFETY OVERRIDE: Automatically assigns a default Scouting logo if a Scout forgets to add an author profile photo. processBlogPosts: async ({ blogPosts }) => { - const DEFAULT_IMAGE = '/img/logos/favicon.png'; + const DEFAULT_IMAGE = "/img/logos/favicon.png"; return blogPosts.map((post) => { if (post.metadata && post.metadata.authors) { @@ -136,18 +139,19 @@ const config = { // 🍪 PRIVACY COMPLIANCE: Optional cookie consent pop-up banner. // Set 'enabled: true' if you decide to activate tracking analytics in the future. [ - 'docusaurus-plugin-cookie-consent', + "docusaurus-plugin-cookie-consent", { - title: 'Cookie Consent', - description: 'We use cookies to enhance your browsing experience and analyze our traffic.', + title: "Cookie Consent", + description: + "We use cookies to enhance your browsing experience and analyze our traffic.", links: [ - { label: 'Privacy Policy', href: '/privacy' }, - { label: 'Cookie Policy', href: '/cookies' }, + { label: "Privacy Policy", href: "/privacy" }, + { label: "Cookie Policy", href: "/cookies" }, ], enabled: false, - acceptAllText: 'Accept All Cookies', - rejectOptionalText: 'Essential Only', - rejectAllText: 'Reject All', + acceptAllText: "Accept All Cookies", + rejectOptionalText: "Essential Only", + rejectAllText: "Reject All", toastMode: true, }, ], @@ -162,13 +166,15 @@ const config = { defaultMode: "light", }, // Configures the header banner alert when active. Controlled by the SHOW_ANNOUNCEMENT toggle at the top of this file. - announcementBar: SHOW_ANNOUNCEMENT ? { - id: "announcement-bar", - content: 'This is an announcement', - backgroundColor: "var(--announcement-bar)", // Links to a color variable set in src/css/custom.css - textColor: "var(--scouting-america-white)", - isCloseable: true, - } : undefined, + announcementBar: SHOW_ANNOUNCEMENT + ? { + id: "announcement-bar", + content: "This is an announcement", + backgroundColor: "var(--announcement-bar)", // Links to a color variable set in src/css/custom.css + textColor: "var(--scouting-america-white)", + isCloseable: true, + } + : undefined, // --- NAVIGATION BAR CONFIGURATION --- navbar: { @@ -222,7 +228,7 @@ const config = { }, { label: "Helpful Links", - to: "/docs/general/helpful-links" + to: "/docs/general/helpful-links", }, ], }, @@ -238,7 +244,7 @@ const config = { YouTube Channel - ` + `, }, { html: ` @@ -249,7 +255,7 @@ const config = { Troop 303 - ` + `, }, { html: ` @@ -260,7 +266,7 @@ const config = { Troop 331 - ` + `, }, { html: ` @@ -271,7 +277,7 @@ const config = { Pack 303 - ` + `, }, { html: ` @@ -282,7 +288,7 @@ const config = { Crew 303 - ` + `, }, ], }, @@ -300,7 +306,7 @@ const config = { Brownsburg, IN 46112
- ` + `, }, { html: ` @@ -311,17 +317,17 @@ const config = { Email Us