Coverage for src / server_list / spec / ogp.py: 32%
57 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-31 11:45 +0000
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-31 11:45 +0000
1#!/usr/bin/env python3
2"""
3OGP (Open Graph Protocol) tag generation for social media sharing.
4"""
6from __future__ import annotations
8import html
9import re
10from pathlib import Path
11from typing import TYPE_CHECKING
12from urllib.parse import urljoin
14if TYPE_CHECKING:
15 from server_list.config import Config
18def escape(text: str) -> str:
19 """Escape HTML special characters."""
20 return html.escape(str(text)) if text else ""
23def generate_ogp_tags(
24 title: str,
25 description: str,
26 url: str,
27 image_url: str | None = None,
28 site_name: str = "サーバー・仮想マシン一覧",
29) -> str:
30 """Generate OGP meta tags as HTML string.
32 Args:
33 title: Page title
34 description: Page description
35 url: Canonical URL
36 image_url: Optional image URL for preview
37 site_name: Site name
39 Returns:
40 HTML string containing OGP meta tags
41 """
42 tags = [
43 f'<meta property="og:title" content="{escape(title)}" />',
44 f'<meta property="og:description" content="{escape(description)}" />',
45 '<meta property="og:type" content="website" />',
46 f'<meta property="og:url" content="{escape(url)}" />',
47 f'<meta property="og:site_name" content="{escape(site_name)}" />',
48 '<meta name="twitter:card" content="summary" />',
49 f'<meta name="twitter:title" content="{escape(title)}" />',
50 f'<meta name="twitter:description" content="{escape(description)}" />',
51 ]
53 if image_url: 53 ↛ 54line 53 didn't jump to line 54 because the condition on line 53 was never true
54 tags.extend([
55 f'<meta property="og:image" content="{escape(image_url)}" />',
56 f'<meta name="twitter:image" content="{escape(image_url)}" />',
57 ])
59 return "\n ".join(tags)
62def generate_top_page_ogp(base_url: str, config: Config | None = None) -> str:
63 """Generate OGP tags for the top page.
65 Args:
66 base_url: Base URL of the application
67 config: Optional config to get machine count
69 Returns:
70 HTML string containing OGP meta tags
71 """
72 machine_count = len(config.machine) if config and config.machine else 0
73 vm_count = sum(len(m.vm) if m.vm else 0 for m in config.machine) if config and config.machine else 0
75 if machine_count > 0:
76 description = (
77 f"物理サーバー {machine_count} 台、仮想マシン {vm_count} 台の"
78 "インフラストラクチャ情報を一覧表示"
79 )
80 else:
81 description = "サーバー・仮想マシンのインフラストラクチャ情報を一覧表示"
83 return generate_ogp_tags(
84 title="サーバー・仮想マシン一覧",
85 description=description,
86 url=urljoin(base_url, "/server-list/"),
87 )
90def generate_machine_page_ogp(
91 base_url: str,
92 machine_name: str,
93 config: Config | None = None,
94 image_dir: Path | None = None,
95) -> str:
96 """Generate OGP tags for a specific machine page.
98 Args:
99 base_url: Base URL of the application
100 machine_name: Name of the machine
101 config: Optional config to get machine details
102 image_dir: Optional path to image directory
104 Returns:
105 HTML string containing OGP meta tags
106 """
107 machine = None
108 if config and config.machine: 108 ↛ 109line 108 didn't jump to line 109 because the condition on line 108 was never true
109 for m in config.machine:
110 if m.name == machine_name:
111 machine = m
112 break
114 if machine: 114 ↛ 116line 114 didn't jump to line 116 because the condition on line 114 was never true
115 # Build description from machine specs
116 specs = [machine.mode, machine.cpu, f"RAM: {machine.ram}"]
117 if machine.vm:
118 specs.append(f"VM: {len(machine.vm)}台")
120 description = " / ".join(specs)
121 title = f"{machine_name} - サーバー詳細"
123 # Check if image exists
124 image_url = None
125 if image_dir:
126 # Normalize model name for image filename
127 image_name = _normalize_model_name(machine.mode)
128 for ext in [".png", ".jpg", ".jpeg", ".webp"]:
129 image_path = image_dir / f"{image_name}{ext}"
130 if image_path.exists():
131 image_url = urljoin(base_url, f"/server-list/api/img/{image_name}{ext}")
132 break
133 else:
134 title = f"{machine_name} - サーバー詳細"
135 description = f"{machine_name} のサーバー情報"
136 image_url = None
138 return generate_ogp_tags(
139 title=title,
140 description=description,
141 url=urljoin(base_url, f"/server-list/machine/{machine_name}"),
142 image_url=image_url,
143 )
146def _normalize_model_name(model: str) -> str:
147 """Normalize model name for image filename lookup.
149 Args:
150 model: Model name (e.g., "HPE ProLiant DL360 Gen10")
152 Returns:
153 Normalized name for filename (e.g., "hpe_proliant_dl360_gen10")
154 """
155 # Convert to lowercase, replace spaces and special chars with underscore
156 normalized = model.lower()
157 normalized = re.sub(r"[^a-z0-9]+", "_", normalized)
158 normalized = re.sub(r"_+", "_", normalized)
159 normalized = normalized.strip("_")
160 return normalized
163def inject_ogp_into_html(html_content: str, ogp_tags: str) -> str:
164 """Inject OGP tags into HTML content.
166 Args:
167 html_content: Original HTML content
168 ogp_tags: OGP meta tags to inject
170 Returns:
171 HTML content with OGP tags injected
172 """
173 # Look for </head> or <!-- OGP --> placeholder
174 if "<!-- OGP -->" in html_content:
175 return html_content.replace("<!-- OGP -->", ogp_tags)
177 # Insert before </head>
178 if "</head>" in html_content:
179 return html_content.replace("</head>", f" {ogp_tags}\n </head>")
181 # Fallback: return original
182 return html_content