Skip to content

Commit 0724b0f

Browse files
luciscursoragent
andcommitted
Add English locale with path prefix and CI deploy
Publish PT at / and EN at /en, link posts via translationKey, and deploy to Cloudflare Workers on push to main. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 6861a1a commit 0724b0f

37 files changed

Lines changed: 998 additions & 75 deletions

.github/workflows/deploy.yml

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
name: Deploy
2+
3+
on:
4+
push:
5+
branches: [main]
6+
workflow_dispatch:
7+
8+
concurrency:
9+
group: deploy-production
10+
cancel-in-progress: true
11+
12+
jobs:
13+
deploy:
14+
runs-on: ubuntu-latest
15+
timeout-minutes: 15
16+
permissions:
17+
contents: read
18+
steps:
19+
- name: Checkout
20+
uses: actions/checkout@v4
21+
22+
- name: Setup Node
23+
uses: actions/setup-node@v4
24+
with:
25+
node-version: "22"
26+
cache: npm
27+
28+
- name: Install dependencies
29+
run: npm ci
30+
31+
- name: Build
32+
run: npm run build
33+
34+
- name: Deploy to Cloudflare Workers
35+
uses: cloudflare/wrangler-action@v4
36+
with:
37+
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
38+
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
39+
command: deploy
40+
packageManager: npm

README.md

Lines changed: 53 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@
22

33
Blog pessoal de Luciano Júnior, construído com [Astro](https://astro.build) e hospedado no Cloudflare Workers.
44

5+
Site: [https://blog.lucis.dev](https://blog.lucis.dev)
6+
Repositório: [github.com/lucis/blog](https://github.com/lucis/blog)
7+
8+
## Idiomas
9+
10+
- Português (padrão): `/`, `/blog/:slug`
11+
- Inglês: `/en`, `/en/blog/:slug`
12+
13+
Cada post MDX declara `lang` (`pt` | `en`) e `translationKey` para ligar as versões. A UI e as datas seguem o idioma da rota. O poema da sidebar permanece em português.
14+
515
## Desenvolvimento
616

717
```sh
@@ -18,29 +28,62 @@ npm run build
1828
npm run preview:cf # wrangler dev com o build estático
1929
```
2030

21-
Use `npm run dev` no dia a dia.
31+
## Conteúdo
2232

23-
## Deploy
33+
Posts ficam em `src/content/blog/` como arquivos MDX.
2434

25-
```sh
26-
npm run deploy
35+
Frontmatter mínimo de um post publicado:
36+
37+
```yaml
38+
title: Meu título
39+
date: '2026-08-02'
40+
lang: pt
41+
translationKey: meu-titulo
42+
draft: false
2743
```
2844
29-
Requer [Wrangler](https://developers.cloudflare.com/workers/wrangler/) autenticado (`wrangler login`). O Worker já está ligado a `blog.lucis.dev`.
45+
A tradução em inglês usa o mesmo `translationKey`, `lang: en` e um slug/arquivo próprio (ex.: `my-title.mdx` → `/en/blog/my-title`).
3046

31-
## Conteúdo
47+
Posts com `draft: true`, ou título contendo `Resumo:` / `Summary:`, não aparecem na home nem nas rotas públicas.
3248

33-
Posts ficam em `src/content/blog/` como arquivos MDX. Para reimportar do CMS Deco legado (JSONs em `scripts/legacy-posts/`):
49+
Para reimportar do CMS Deco legado (JSONs em `scripts/legacy-posts/`):
3450

3551
```sh
3652
npm run migrate
3753
```
3854

39-
Posts com `draft: true` ou título contendo "Resumo:" não aparecem na home.
55+
## Deploy
56+
57+
### Automático (GitHub Actions)
58+
59+
Todo push em `main` (e disparos manuais via *Actions → Deploy → Run workflow*) faz build e publica no Worker `lucispersonalblog`, ligado a `blog.lucis.dev`.
60+
61+
Workflow: [`.github/workflows/deploy.yml`](.github/workflows/deploy.yml)
62+
63+
Secrets necessários no repositório (`Settings → Secrets and variables → Actions`):
64+
65+
| Secret | Valor |
66+
|--------|--------|
67+
| `CLOUDFLARE_ACCOUNT_ID` | ID da conta Cloudflare |
68+
| `CLOUDFLARE_API_TOKEN` | API Token com permissão **Edit Cloudflare Workers** |
69+
70+
Crie o token em [Account API tokens](https://dash.cloudflare.com/profile/api-tokens) com o template **Edit Cloudflare Workers**, escopado à conta do blog.
71+
72+
### Manual
73+
74+
```sh
75+
npm run deploy
76+
```
77+
78+
Requer [Wrangler](https://developers.cloudflare.com/workers/wrangler/) autenticado (`wrangler login`).
4079

4180
## Estrutura
4281

43-
- `src/pages/` — rotas (`/`, `/blog/[slug]`)
44-
- `src/components/` — Sidebar, PostCard, Footer
82+
- `src/pages/` — rotas (`/`, `/blog/[slug]`, `/en`, `/en/blog/[slug]`)
83+
- `src/content/blog/` — posts MDX (PT e EN)
84+
- `src/lib/i18n.ts` — strings e helpers de locale
85+
- `src/lib/posts.ts` — coleta, recomendações e links de tradução
86+
- `src/components/` — Sidebar, LanguageSwitcher, PostCard, Footer
4587
- `src/layouts/` — BaseLayout, BlogPostLayout
4688
- `scripts/migrate-posts.mjs` — conversão JSON → MDX (one-shot)
89+
- `.github/workflows/deploy.yml` — CI/CD para Cloudflare Workers

astro.config.mjs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,11 @@ export default defineConfig({
66
site: "https://blog.lucis.dev",
77
output: "static",
88
integrations: [mdx(), tailwind({ applyBaseStyles: false })],
9+
i18n: {
10+
defaultLocale: "pt",
11+
locales: ["pt", "en"],
12+
routing: {
13+
prefixDefaultLocale: false,
14+
},
15+
},
916
});
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
---
2+
import type { Locale } from "../lib/i18n";
3+
import { localePath } from "../lib/i18n";
4+
5+
interface Props {
6+
locale: Locale;
7+
ptHref?: string | null;
8+
enHref?: string | null;
9+
}
10+
11+
const { locale, ptHref, enHref } = Astro.props;
12+
13+
const ptLink = ptHref ?? localePath("pt", "/");
14+
const enLink = enHref ?? localePath("en", "/");
15+
---
16+
17+
<div class="flex items-center justify-center gap-3 text-sm font-serif tracking-wide">
18+
<a
19+
href={ptLink}
20+
class:list={[
21+
"transition-opacity hover:opacity-100",
22+
locale === "pt" ? "opacity-100 underline underline-offset-4" : "opacity-40",
23+
]}
24+
aria-current={locale === "pt" ? "page" : undefined}
25+
>
26+
PT
27+
</a>
28+
<span class="opacity-30" aria-hidden="true">/</span>
29+
<a
30+
href={enLink}
31+
class:list={[
32+
"transition-opacity hover:opacity-100",
33+
locale === "en" ? "opacity-100 underline underline-offset-4" : "opacity-40",
34+
]}
35+
aria-current={locale === "en" ? "page" : undefined}
36+
>
37+
EN
38+
</a>
39+
</div>

src/components/PostCard.astro

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
---
22
import type { BlogEntry } from "../lib/posts";
3-
import { getPostSlug } from "../lib/posts";
3+
import { getPostHref } from "../lib/posts";
44
55
interface Props {
66
post: BlogEntry;
77
}
88
99
const { post } = Astro.props;
1010
const { title, image } = post.data;
11-
const slug = getPostSlug(post);
11+
const href = getPostHref(post);
1212
const hasImage = Boolean(image?.trim());
1313
---
1414

1515
<article class="border border-gray-100 rounded-lg overflow-hidden hover:bg-gray-50 transition-colors duration-200 delay-75">
16-
<a href={`/blog/${slug}`} class="block p-4 lg:p-6">
16+
<a href={href} class="block p-4 lg:p-6">
1717
<div class="lg:hidden">
1818
{
1919
hasImage && (

src/components/RecommendedPosts.astro

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,34 @@
11
---
22
import type { BlogEntry } from "../lib/posts";
3-
import { getPostSlug } from "../lib/posts";
3+
import { getPostHref } from "../lib/posts";
4+
import type { Locale } from "../lib/i18n";
5+
import { t } from "../lib/i18n";
46
57
interface Props {
68
posts: BlogEntry[];
9+
locale?: Locale;
710
}
811
9-
const { posts } = Astro.props;
12+
const { posts, locale = "pt" } = Astro.props;
13+
const copy = t(locale);
1014
---
1115

1216
{
1317
posts.length > 0 && (
1418
<section class="mt-16 max-w-3xl mx-auto w-full px-4 md:px-0">
1519
<div class="text-center mb-8">
1620
<span class="text-black text-2xl">•</span>
17-
<h2 class="mt-4 text-2xl font-bold font-serif text-black">Ler mais</h2>
21+
<h2 class="mt-4 text-2xl font-bold font-serif text-black">{copy.readMore}</h2>
1822
</div>
1923

2024
<div class="hidden md:block space-y-3">
2125
{posts.map((post) => {
22-
const slug = getPostSlug(post);
26+
const href = getPostHref(post);
2327
const hasImage = Boolean(post.data.image?.trim());
2428

2529
return (
2630
<article class="border border-gray-100 rounded-lg overflow-hidden hover:bg-gray-50 transition-colors duration-200 delay-75">
27-
<a href={`/blog/${slug}`} class="block p-6">
31+
<a href={href} class="block p-6">
2832
<div class="flex gap-4 items-center">
2933
{hasImage && (
3034
<div class="w-20 h-20 flex-shrink-0">
@@ -53,11 +57,11 @@ const { posts } = Astro.props;
5357

5458
<div class="md:hidden grid gap-6 grid-cols-1 sm:grid-cols-2">
5559
{posts.map((post) => {
56-
const slug = getPostSlug(post);
60+
const href = getPostHref(post);
5761
const hasImage = Boolean(post.data.image?.trim());
5862

5963
return (
60-
<a href={`/blog/${slug}`} class="block group">
64+
<a href={href} class="block group">
6165
<div class="space-y-2">
6266
{hasImage && (
6367
<img

src/components/Sidebar.astro

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,17 @@
11
---
2+
import type { Locale } from "../lib/i18n";
3+
import { t } from "../lib/i18n";
4+
import LanguageSwitcher from "./LanguageSwitcher.astro";
5+
6+
interface Props {
7+
locale?: Locale;
8+
ptHref?: string | null;
9+
enHref?: string | null;
10+
}
11+
12+
const { locale = "pt", ptHref, enHref } = Astro.props;
13+
const copy = t(locale);
14+
215
const POEM_LINES = [
316
"Sou poeira que o vento não levou",
417
"Sou caminho que só o amor pisou",
@@ -31,6 +44,8 @@ const AVATAR_URL =
3144

3245
<h1 class="text-2xl font-bold text-black font-serif">Luciano Júnior</h1>
3346

47+
<LanguageSwitcher locale={locale} ptHref={ptHref} enHref={enHref} />
48+
3449
<div class="mt-8 max-w-xs mx-auto">
3550
<blockquote class="relative px-4">
3651
<span
@@ -73,7 +88,7 @@ const AVATAR_URL =
7388
href={link.url}
7489
target="_blank"
7590
rel="noopener noreferrer"
76-
aria-label={`Visitar ${link.label}`}
91+
aria-label={copy.visitSocial(link.label)}
7792
class="group relative p-2 rounded-full transition-all duration-300 ease-out hover:scale-110 opacity-50 hover:opacity-100 text-black"
7893
>
7994
<span class="absolute inset-0 rounded-full bg-black opacity-0 group-hover:opacity-5 transition-opacity duration-300" />

src/content.config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ const blog = defineCollection({
2424
.default([]),
2525
excerpt: z.string().optional(),
2626
draft: z.boolean().default(false),
27+
lang: z.enum(["pt", "en"]).default("pt"),
28+
translationKey: z.string(),
2729
}),
2830
});
2931

src/content/blog/a-historia-na-qual-o-brasil-ganhou.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
---
22
title: A história na qual o Brasil ganhou
33
date: '2024-06-11'
4+
lang: pt
5+
translationKey: a-historia-na-qual-o-brasil-ganhou
46
authors:
57
- name: Luciano Júnior
68
avatar: https://assets.decocache.com/lucispersonalblog/f60b44ce-b752-45c9-9a4b-ebf919f956eb/lucis2244_passaro_--sref_httpss.mj.rundzR6R6mvHVw_--v_6_9ea9bdc7-bc75-4164-b565-6fc4378eb3df-(1).png

src/content/blog/andrew-ng-building-faster-with-ai.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
---
22
title: 'Resumo: Andrew Ng - Building Faster with AI'
33
date: '2025-07-09'
4+
lang: pt
5+
translationKey: andrew-ng-building-faster-with-ai
46
authors:
57
- name: Luciano Júnior
68
avatar: https://assets.decocache.com/lucispersonalblog/f60b44ce-b752-45c9-9a4b-ebf919f956eb/lucis2244_passaro_--sref_httpss.mj.rundzR6R6mvHVw_--v_6_9ea9bdc7-bc75-4164-b565-6fc4378eb3df-(1).png

0 commit comments

Comments
 (0)