feat: SEO optimization and dynamic jobs API integration
Backend: - Add Swagger annotations to all job handlers (GET, POST, PUT, DELETE) - Clean up job handler code Frontend: - Expand api.ts with ApiJob types, pagination, and transform function - Update footer with 'Vagas por Tecnologia' SEO links - Add robots.txt with crawler directives - Add sitemap.xml with main pages and job URLs - Change branding to GoHorse Jobs
This commit is contained in:
parent
8856357acd
commit
a4abcf8e05
12 changed files with 284 additions and 38 deletions
|
|
@ -17,6 +17,18 @@ func NewJobHandler(service *services.JobService) *JobHandler {
|
|||
return &JobHandler{Service: service}
|
||||
}
|
||||
|
||||
// GetJobs godoc
|
||||
// @Summary List all jobs
|
||||
// @Description Get a paginated list of job postings with optional filters
|
||||
// @Tags Jobs
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param page query int false "Page number (default: 1)"
|
||||
// @Param limit query int false "Items per page (default: 10, max: 100)"
|
||||
// @Param companyId query int false "Filter by company ID"
|
||||
// @Success 200 {object} dto.PaginatedResponse
|
||||
// @Failure 500 {string} string "Internal Server Error"
|
||||
// @Router /jobs [get]
|
||||
func (h *JobHandler) GetJobs(w http.ResponseWriter, r *http.Request) {
|
||||
page, _ := strconv.Atoi(r.URL.Query().Get("page"))
|
||||
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
|
||||
|
|
@ -51,6 +63,17 @@ func (h *JobHandler) GetJobs(w http.ResponseWriter, r *http.Request) {
|
|||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// CreateJob godoc
|
||||
// @Summary Create a new job
|
||||
// @Description Create a new job posting
|
||||
// @Tags Jobs
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param job body dto.CreateJobRequest true "Job data"
|
||||
// @Success 201 {object} models.Job
|
||||
// @Failure 400 {string} string "Bad Request"
|
||||
// @Failure 500 {string} string "Internal Server Error"
|
||||
// @Router /jobs [post]
|
||||
func (h *JobHandler) CreateJob(w http.ResponseWriter, r *http.Request) {
|
||||
var req dto.CreateJobRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
|
|
@ -71,29 +94,19 @@ func (h *JobHandler) CreateJob(w http.ResponseWriter, r *http.Request) {
|
|||
json.NewEncoder(w).Encode(job)
|
||||
}
|
||||
|
||||
// GetJobByID godoc
|
||||
// @Summary Get job by ID
|
||||
// @Description Get a single job posting by its ID
|
||||
// @Tags Jobs
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int true "Job ID"
|
||||
// @Success 200 {object} models.Job
|
||||
// @Failure 400 {string} string "Bad Request"
|
||||
// @Failure 404 {string} string "Not Found"
|
||||
// @Router /jobs/{id} [get]
|
||||
func (h *JobHandler) GetJobByID(w http.ResponseWriter, r *http.Request) {
|
||||
idStr := r.PathValue("id") // Go 1.22+ routing
|
||||
if idStr == "" {
|
||||
// Fallback for older Go versions or if not using PathValue compatible mux
|
||||
// But let's assume standard mux or we might need to parse URL
|
||||
// For now, let's assume we can get it from context or URL
|
||||
// If using standard http.ServeMux in Go 1.22, PathValue works.
|
||||
// If not, we might need a helper.
|
||||
// Let's assume standard mux with Go 1.22 for now as it's modern.
|
||||
// If not, we'll fix it.
|
||||
// Actually, let's check go.mod version.
|
||||
}
|
||||
|
||||
// Wait, I should check go.mod version to be safe.
|
||||
// But let's write standard code.
|
||||
|
||||
// Assuming we use a router that puts params in context or we parse URL.
|
||||
// Since I am editing router.go later, I can ensure we use Go 1.22 patterns or a library.
|
||||
// The existing router.go used `mux.HandleFunc("/jobs", ...)` which suggests standard lib.
|
||||
|
||||
// Let's try to parse from URL path if PathValue is not available (it is in Go 1.22).
|
||||
// I'll use a helper to extract ID from URL for now to be safe if I'm not sure about Go version.
|
||||
// But `r.PathValue` is the way forward.
|
||||
|
||||
id, err := strconv.Atoi(idStr)
|
||||
if err != nil {
|
||||
|
|
@ -111,6 +124,18 @@ func (h *JobHandler) GetJobByID(w http.ResponseWriter, r *http.Request) {
|
|||
json.NewEncoder(w).Encode(job)
|
||||
}
|
||||
|
||||
// UpdateJob godoc
|
||||
// @Summary Update a job
|
||||
// @Description Update an existing job posting
|
||||
// @Tags Jobs
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int true "Job ID"
|
||||
// @Param job body dto.UpdateJobRequest true "Updated job data"
|
||||
// @Success 200 {object} models.Job
|
||||
// @Failure 400 {string} string "Bad Request"
|
||||
// @Failure 500 {string} string "Internal Server Error"
|
||||
// @Router /jobs/{id} [put]
|
||||
func (h *JobHandler) UpdateJob(w http.ResponseWriter, r *http.Request) {
|
||||
idStr := r.PathValue("id")
|
||||
id, err := strconv.Atoi(idStr)
|
||||
|
|
@ -135,6 +160,17 @@ func (h *JobHandler) UpdateJob(w http.ResponseWriter, r *http.Request) {
|
|||
json.NewEncoder(w).Encode(job)
|
||||
}
|
||||
|
||||
// DeleteJob godoc
|
||||
// @Summary Delete a job
|
||||
// @Description Delete a job posting
|
||||
// @Tags Jobs
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int true "Job ID"
|
||||
// @Success 204 "No Content"
|
||||
// @Failure 400 {string} string "Bad Request"
|
||||
// @Failure 500 {string} string "Internal Server Error"
|
||||
// @Router /jobs/{id} [delete]
|
||||
func (h *JobHandler) DeleteJob(w http.ResponseWriter, r *http.Request) {
|
||||
idStr := r.PathValue("id")
|
||||
id, err := strconv.Atoi(idStr)
|
||||
|
|
|
|||
15
frontend/public/robots.txt
Normal file
15
frontend/public/robots.txt
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
# Sitemap
|
||||
Sitemap: https://gohorsejobs.com/sitemap.xml
|
||||
|
||||
# Crawl-delay for polite bots
|
||||
Crawl-delay: 1
|
||||
|
||||
# Disallow admin/dashboard areas from indexing
|
||||
Disallow: /dashboard/
|
||||
Disallow: /api/
|
||||
Disallow: /login
|
||||
Disallow: /cadastro/
|
||||
82
frontend/public/sitemap.xml
Normal file
82
frontend/public/sitemap.xml
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
<!-- Main Pages -->
|
||||
<url>
|
||||
<loc>https://gohorsejobs.com/</loc>
|
||||
<lastmod>2025-12-14</lastmod>
|
||||
<changefreq>daily</changefreq>
|
||||
<priority>1.0</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://gohorsejobs.com/vagas</loc>
|
||||
<lastmod>2025-12-14</lastmod>
|
||||
<changefreq>daily</changefreq>
|
||||
<priority>0.9</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://gohorsejobs.com/sobre</loc>
|
||||
<lastmod>2025-12-14</lastmod>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.6</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://gohorsejobs.com/contato</loc>
|
||||
<lastmod>2025-12-14</lastmod>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.6</priority>
|
||||
</url>
|
||||
|
||||
<!-- Vagas por Tecnologia -->
|
||||
<url>
|
||||
<loc>https://gohorsejobs.com/vagas?tech=python</loc>
|
||||
<lastmod>2025-12-14</lastmod>
|
||||
<changefreq>daily</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://gohorsejobs.com/vagas?tech=react</loc>
|
||||
<lastmod>2025-12-14</lastmod>
|
||||
<changefreq>daily</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://gohorsejobs.com/vagas?tech=nodejs</loc>
|
||||
<lastmod>2025-12-14</lastmod>
|
||||
<changefreq>daily</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://gohorsejobs.com/vagas?tech=dados</loc>
|
||||
<lastmod>2025-12-14</lastmod>
|
||||
<changefreq>daily</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
|
||||
<!-- Vagas por Tipo -->
|
||||
<url>
|
||||
<loc>https://gohorsejobs.com/vagas?type=remoto</loc>
|
||||
<lastmod>2025-12-14</lastmod>
|
||||
<changefreq>daily</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://gohorsejobs.com/vagas?type=full-time</loc>
|
||||
<lastmod>2025-12-14</lastmod>
|
||||
<changefreq>daily</changefreq>
|
||||
<priority>0.7</priority>
|
||||
</url>
|
||||
|
||||
<!-- Legal -->
|
||||
<url>
|
||||
<loc>https://gohorsejobs.com/privacidade</loc>
|
||||
<lastmod>2025-12-14</lastmod>
|
||||
<changefreq>yearly</changefreq>
|
||||
<priority>0.3</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://gohorsejobs.com/termos</loc>
|
||||
<lastmod>2025-12-14</lastmod>
|
||||
<changefreq>yearly</changefreq>
|
||||
<priority>0.3</priority>
|
||||
</url>
|
||||
</urlset>
|
||||
|
|
@ -1,52 +1,76 @@
|
|||
import Link from "next/link"
|
||||
import { Briefcase } from "lucide-react"
|
||||
|
||||
export function Footer() {
|
||||
return (
|
||||
<footer className="border-t border-border bg-muted/30 mt-24">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-8">
|
||||
<div className="grid grid-cols-1 md:grid-cols-5 gap-8">
|
||||
<div className="col-span-1 md:col-span-2">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
|
||||
<span className="text-xl font-semibold">Portal de Empregos</span>
|
||||
<span className="text-xl font-semibold">GoHorse Jobs</span>
|
||||
</div>
|
||||
<p className="text-muted-foreground text-sm leading-relaxed max-w-md">
|
||||
Conectamos candidatos e empresas de forma rápida e direta. Encontre sua próxima oportunidade profissional.
|
||||
Conectamos candidatos e empresas de forma rápida e direta. Encontre sua próxima oportunidade profissional em tecnologia.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="font-semibold mb-4">Empresa</h3>
|
||||
<h3 className="font-semibold mb-4">Vagas por Tecnologia</h3>
|
||||
<ul className="space-y-2 text-sm text-muted-foreground">
|
||||
<li>
|
||||
<Link href="#" className="hover:text-foreground transition-colors">
|
||||
Sobre
|
||||
<Link href="/vagas?tech=python" className="hover:text-foreground transition-colors">
|
||||
Desenvolvedor Python
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="#" className="hover:text-foreground transition-colors">
|
||||
Contato
|
||||
<Link href="/vagas?tech=react" className="hover:text-foreground transition-colors">
|
||||
Desenvolvedor React
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="#" className="hover:text-foreground transition-colors">
|
||||
Carreiras
|
||||
<Link href="/vagas?tech=dados" className="hover:text-foreground transition-colors">
|
||||
Analista de Dados
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/vagas?type=remoto" className="hover:text-foreground transition-colors">
|
||||
Vagas Remotas
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="font-semibold mb-4">Privacidade e Termos</h3>
|
||||
<h3 className="font-semibold mb-4">Empresa</h3>
|
||||
<ul className="space-y-2 text-sm text-muted-foreground">
|
||||
<li>
|
||||
<Link href="#" className="hover:text-foreground transition-colors">
|
||||
<Link href="/sobre" className="hover:text-foreground transition-colors">
|
||||
Sobre
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/contato" className="hover:text-foreground transition-colors">
|
||||
Contato
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/vagas" className="hover:text-foreground transition-colors">
|
||||
Todas as Vagas
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="font-semibold mb-4">Legal</h3>
|
||||
<ul className="space-y-2 text-sm text-muted-foreground">
|
||||
<li>
|
||||
<Link href="/privacidade" className="hover:text-foreground transition-colors">
|
||||
Política de Privacidade
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="#" className="hover:text-foreground transition-colors">
|
||||
<Link href="/termos" className="hover:text-foreground transition-colors">
|
||||
Termos de Uso
|
||||
</Link>
|
||||
</li>
|
||||
|
|
@ -55,9 +79,10 @@ export function Footer() {
|
|||
</div>
|
||||
|
||||
<div className="mt-12 pt-8 border-t border-border text-center text-sm text-muted-foreground">
|
||||
<p>© 2025 Portal de Empregos. Todos os direitos reservados.</p>
|
||||
<p>© 2025 GoHorse Jobs. Todos os direitos reservados.</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -97,6 +97,94 @@ export const companiesApi = {
|
|||
};
|
||||
|
||||
// Jobs API (public)
|
||||
export const jobsApi = {
|
||||
list: () => apiRequest<unknown[]>("/jobs"),
|
||||
export interface ApiJob {
|
||||
id: number;
|
||||
companyId: number;
|
||||
createdBy: number;
|
||||
title: string;
|
||||
description: string;
|
||||
salaryMin?: number;
|
||||
salaryMax?: number;
|
||||
salaryType?: string;
|
||||
employmentType?: string;
|
||||
workingHours?: string;
|
||||
location?: string;
|
||||
regionId?: number;
|
||||
cityId?: number;
|
||||
requirements?: Record<string, unknown> | string[];
|
||||
benefits?: Record<string, unknown> | string[];
|
||||
visaSupport: boolean;
|
||||
languageLevel?: string;
|
||||
status: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
companyName?: string;
|
||||
companyLogoUrl?: string;
|
||||
regionName?: string;
|
||||
cityName?: string;
|
||||
}
|
||||
|
||||
export interface PaginatedResponse<T> {
|
||||
data: T[];
|
||||
pagination: {
|
||||
page: number;
|
||||
limit: number;
|
||||
total: number;
|
||||
};
|
||||
}
|
||||
|
||||
export const jobsApi = {
|
||||
list: (params?: { page?: number; limit?: number; companyId?: number }) => {
|
||||
const query = new URLSearchParams();
|
||||
if (params?.page) query.set('page', String(params.page));
|
||||
if (params?.limit) query.set('limit', String(params.limit));
|
||||
if (params?.companyId) query.set('companyId', String(params.companyId));
|
||||
const queryStr = query.toString();
|
||||
return apiRequest<PaginatedResponse<ApiJob>>(`/jobs${queryStr ? `?${queryStr}` : ''}`);
|
||||
},
|
||||
|
||||
getById: (id: number) => apiRequest<ApiJob>(`/jobs/${id}`),
|
||||
};
|
||||
|
||||
// Transform API job to frontend Job format
|
||||
export function transformApiJobToFrontend(apiJob: ApiJob): import('./types').Job {
|
||||
// Format salary
|
||||
let salary: string | undefined;
|
||||
if (apiJob.salaryMin && apiJob.salaryMax) {
|
||||
salary = `R$ ${apiJob.salaryMin.toLocaleString('pt-BR')} - R$ ${apiJob.salaryMax.toLocaleString('pt-BR')}`;
|
||||
} else if (apiJob.salaryMin) {
|
||||
salary = `A partir de R$ ${apiJob.salaryMin.toLocaleString('pt-BR')}`;
|
||||
} else if (apiJob.salaryMax) {
|
||||
salary = `Até R$ ${apiJob.salaryMax.toLocaleString('pt-BR')}`;
|
||||
}
|
||||
|
||||
// Determine type
|
||||
type JobType = 'full-time' | 'part-time' | 'contract' | 'Remoto' | 'Tempo Integral';
|
||||
let type: JobType = 'Tempo Integral';
|
||||
if (apiJob.employmentType === 'full-time') type = 'Tempo Integral';
|
||||
else if (apiJob.employmentType === 'part-time') type = 'part-time';
|
||||
else if (apiJob.employmentType === 'contract') type = 'contract';
|
||||
else if (apiJob.location?.toLowerCase().includes('remoto')) type = 'Remoto';
|
||||
|
||||
// Extract requirements
|
||||
const requirements: string[] = [];
|
||||
if (apiJob.requirements) {
|
||||
if (Array.isArray(apiJob.requirements)) {
|
||||
requirements.push(...apiJob.requirements.map(String));
|
||||
} else if (typeof apiJob.requirements === 'object') {
|
||||
Object.values(apiJob.requirements).forEach(v => requirements.push(String(v)));
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: String(apiJob.id),
|
||||
title: apiJob.title,
|
||||
company: apiJob.companyName || 'Empresa',
|
||||
location: apiJob.location || apiJob.cityName || 'Localização não informada',
|
||||
type,
|
||||
salary,
|
||||
description: apiJob.description,
|
||||
requirements: requirements.length > 0 ? requirements : ['Ver detalhes'],
|
||||
postedAt: apiJob.createdAt?.split('T')[0] || new Date().toISOString().split('T')[0],
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue