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

1#!/usr/bin/env python3 

2""" 

3OGP (Open Graph Protocol) tag generation for social media sharing. 

4""" 

5 

6from __future__ import annotations 

7 

8import html 

9import re 

10from pathlib import Path 

11from typing import TYPE_CHECKING 

12from urllib.parse import urljoin 

13 

14if TYPE_CHECKING: 

15 from server_list.config import Config 

16 

17 

18def escape(text: str) -> str: 

19 """Escape HTML special characters.""" 

20 return html.escape(str(text)) if text else "" 

21 

22 

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. 

31 

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 

38 

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 ] 

52 

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 ]) 

58 

59 return "\n ".join(tags) 

60 

61 

62def generate_top_page_ogp(base_url: str, config: Config | None = None) -> str: 

63 """Generate OGP tags for the top page. 

64 

65 Args: 

66 base_url: Base URL of the application 

67 config: Optional config to get machine count 

68 

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 

74 

75 if machine_count > 0: 

76 description = ( 

77 f"物理サーバー {machine_count} 台、仮想マシン {vm_count} 台の" 

78 "インフラストラクチャ情報を一覧表示" 

79 ) 

80 else: 

81 description = "サーバー・仮想マシンのインフラストラクチャ情報を一覧表示" 

82 

83 return generate_ogp_tags( 

84 title="サーバー・仮想マシン一覧", 

85 description=description, 

86 url=urljoin(base_url, "/server-list/"), 

87 ) 

88 

89 

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. 

97 

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 

103 

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 

113 

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)}") 

119 

120 description = " / ".join(specs) 

121 title = f"{machine_name} - サーバー詳細" 

122 

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 

137 

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 ) 

144 

145 

146def _normalize_model_name(model: str) -> str: 

147 """Normalize model name for image filename lookup. 

148 

149 Args: 

150 model: Model name (e.g., "HPE ProLiant DL360 Gen10") 

151 

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 

161 

162 

163def inject_ogp_into_html(html_content: str, ogp_tags: str) -> str: 

164 """Inject OGP tags into HTML content. 

165 

166 Args: 

167 html_content: Original HTML content 

168 ogp_tags: OGP meta tags to inject 

169 

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) 

176 

177 # Insert before </head> 

178 if "</head>" in html_content: 

179 return html_content.replace("</head>", f" {ogp_tags}\n </head>") 

180 

181 # Fallback: return original 

182 return html_content